Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Знакомство с XNA Framework и XNA Game Studio  »  XNA - Создание 3D-объектов

XNA Game Studio: Создание 3D-объектов в MSVC#2008




Здесь мы рассмотрим основные элементы трёхмерной графики и как именно XNA Framework представляет (exposes) различные типы данных и объекты, которые позволят нам с лёгкостью создавать 3D-миры.1 Мы создадим несколько приложений с 3D-демо, которые позволят наглядно увидеть изучаемый материал. Все объекты мы создадим посредством исходного кода, без применения пакетов 3D-моделирования (на первых порах). В конце статьи мы узнаем, как перемещать объекты в окне приложения.

Вершины (Vertices)

Абсолютно всё в 3D-игре представлено в виде 3D-точек (См. Рис.1). Существует два основных способа вывода 3D-объектов на экран: набросать (plot) точки самостоятельно, либо загрузить их из готового файла 3D-модели (в котором сохранены все точки, составляющие модель). В данной статье мы будем "вручную" (т.е. с помощью нашей IDE) создавать и размещать точки, называемые в 3D-программировании вершинами (vertices).
Мы определяем положение вершин в 3D-пространстве с помощью пространственных координат x, y и z.


Рис. 1 Вершины, рёбра и грани - основные элементы для создания 3D-объектов
Рис. 1 Вершины, рёбра и грани - основные элементы для создания 3D-объектов



Векторы (Vectors)

Из любой вершины можно провести вектор. И не один. XNA предлагает аж три структуры для хранения векторов:
  • Vector2 (имеет только координаты x, y);
  • Vector3 (имеет координаты x, y, z);
  • Vector4 (обычно применяется для хранения и передачи данных о цвете)
2D-векторы (Vector2) обычно применяются в двумерных играх, а также при работе с 3D-текстурами. Помимо вершин, векторы могут также хранить скорость (velocity).
Над векторами можно производить различные математические операции. В XNA встроено множество функций-помощников, выполняющие эти операции без необходимости глубокого копания в исходном коде.

Матрицы (Matrices)

В XNA матрицей нзывается таблица данных размером 4Х4. Это двумерный массив. Матрица идентичности (identity matrix), она же единичная матрица (unit matrix), схожа с числом 1. К примеру если мы перемножим любое другое число на 1, то в результате всегда получим исходное число (5 * 1 = 5).
Произведением произвольной матрицы и единичной матрицы является та же самая исходная произвольная матрица. Единичная матрица очень широко применяется как сама по себе, так и совмествно с другими матрицами. Её отдельные поля структурированы по строкам и столбцам вида 4х4. Это позволяет выполнять внутри одной матрицы множество различных преобразований (трансформаций, transformations). Для хранения матриц XNA предоставляет структуру Matrix.

Трансформации (Transformations)

Матрица данных как правило содержит в себе т.н. трансформацию. В игрокодинге существуют 3 основных вида трансформаций (преобразований):
  • Трансляция (Translation; изменение положения в пространстве);
  • Масштабирование (Scaling);
  • Вращение (Rotation).
Все они применяются для соответствующих трансформаций 3D-объектов.

Трансляция (Translation; изменение положения в пространстве)

Трансляция означает обычное перемещение объектов в 3D-пространстве. Мы транслируем объект из одной точки в другую путём корректного перемещения каждой точки объекта.

Масштабирование (Scaling)

Масштабирование объекта делает объекты больше или меньше. Это достигается за счёт изменения расстояний между точками объекта.

Вращение (Rotation)

Вращение объекта вращает объект по одной или нескольким осям. Перемещая точки объекта соответствующим образом в 3D-пространстве мы можем заставить наш объект крутиться словно волчок.

Закрыть
noteОбрати внимание

Трансляция является одной из трёх трансформаций. Наряду с масштабированием и вращением.


Трансформации (продолжение)

К объекту в данный момент времени может быть применена одна или несколько трансформаций одновременно. К примеру нам необходимо только транслировать (=переместить) объект, для чего достаточно обновить (update) мировую матрицу объекта (object's world matrix). Затем нам может понадобиться повернуть объект под одной из осей, для чего применим к нему трансформацию вращения. Обычным делом является масштабирование (изменение размера объекта) после его загрузки из 3D-редактора в игровой мир. Здесь применяется трансформация масштабирования. Часто все эти трансформации применяют одновременно. Иногда объект необходимо повернуть не более чем на 5 градусов. И матрица вращения позволяет это с лёгкостью сделать.
Мы может применять к объекту множество различных трансформаций путём перемножения (multiplaying) различных матриц друг на друга. Но здесь есть свои "подводные камни". Мы перемножаем матрицы друг на друга для достижения желаемого результата. Но в отличие от числовых множителей, когда от перестановки множителей произведение не изменяется, при перемножении матриц их порядок имеет ключевое значение. Т.е. Матрица A * Матрица B != (не равно) Матрица B * Матрица A.

Создание камеры

Хватит теории. Создадим камеру, через которую мы сможем увидеть наш 3D-мир.


Рис. 2 Выбираем шаблон будущего игрового проекта.
Рис. 2 Выбираем шаблон будущего игрового проекта.


  • Стартуй MSVC#2008, если не сделал этого ранее. Создай новый проект.
  • В окне New Project выбери Windows Game (3.1) (См. Рис.2).
  • Введи имя XNADemo. Жми OK.

  • В открывшемся файле исходного кода Game1.cs найди следующий фрагмент:
Фрагмент файла Game1.cs
...
namespace XNADemo
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
...


  • Сразу после строки SpriteBatch spriteBatch; добавь ниже следующие строки, объявляющие закрытые члены игрового класса:
private Matrix projection;
private Matrix view;

  • Там же найди функцию Initialize, где инициализируются разные штуки:
Фрагмент файла Game1.cs
...
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }
...

  • ПЕРЕД строкой base.Initialize(); добавь объявление метода, инициализирующего нашу камеру:
InitializeCamera();
Как видим, данный метод без параметров и не возвращает значения. Чуть позднее мы его расширим и сделаем закрытым (private).

  • Сохрани Решение (File -> Save All).

Реализация метода InitializeCamera();


После добавления объявления метода InitializeCamera(), функция Initialize() выглядит так:
Фрагмент файла Game1.cs
...
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            InitializeCamera();

            base.Initialize();
        }
...


  • Сразу после функции Initialize() пишем реализацию метода InitializeCamera():
Фрагмент файла Game1.cs
...
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            InitializeCamera();

            base.Initialize();
        }

        private void InitializeCamera()
        {
            //Projection
            float aspectRatio = (float)graphics.GraphicsDevice.Viewport.Width /
                (float)graphics.GraphicsDevice.Viewport.Height;
            Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio,
                0.0001f, 1000.0f, out projection);

            //View
            Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget,
                ref cameraUpVector, out view);
        }
...


Проекция (Projection)

Первым делом мы вводим новую переменную aspectRatio для хранения соотношения сторон экрана. Если мы устанавливаем широкоэкранное соотношение сторон (16:9 или 16:10), то игроки на кинескопных мониторах с соотношением 4:3 увидят игру с чёрными полосами сверху и снизу. И наоборот, если мы установим соотношение 4:3, а у игрока современный монитор с соотношением 16:9, то игра автоматически растянет изображение, что может привести к нежелательному изменению пропорций 3D-объектов.
Во избежание обоих случаев мы предусмотрим обе ситуации и затем предложим игроку выбрать соотношение сторон его монитора, чтобы оно соответствовало соотношению сторон вьюпорта (=виртуальной камеры, отображающей содержимое игровой 3D-сцены). В XNA даже есть специальное свойство DisplayMode, запрашивающее текущие установки экрана после вызова метода Initialization. Конечно, при необходимости можно форсировать вывод изображения с определённым соотношением. Об этом читай раздел "How to: Restrict Graphics Devices to Widescreen Aspect Ratios in Full Screen" документации XNA Framework.


Рис.3 Усечённая пирамида видимого пространства (View Frustum)
Рис.3 Усечённая пирамида видимого пространства (View Frustum)



Далее создаём поле видимости (field of view) путём вызова метода CreatePerspectiveFieldOfView.
В первом параметре передаём значение MathHelper.PiOver4, что соответствует 45 градусам. Для наглядности можно использовать функцию MathHelper.ToRadians(45.0f), но делать это не обязательно, т.к. в классе MathHelper данной константе по умолчанию присвоено значение 45 градусов.
Второй параметр - переменная aspectRatio, который мы высчитали чуть выше.
Третий и четвёртый параметры - это наши ближняя (передняя) и дальняя (задняя) плоскости отсечения (clipping planes) (См. Рис.3). Указанные здесь значения определяют, насколько удалена каждая из плоскостей от объектива виртуальной камеры. Это необходимо для отсечения всех объектов, расположенных ближе передней плоскости и дальше задней. То есть рендериться будут только объекты, оказавшиеся между этими двумя плоскостями, образующими поле видимости (field of view, FOV).
В последнем параметре (out projection) указывается матрица проекции (projection matrix), над которой и производятся текущие преобразования.
Метод является перегруженным (overloaded). Вообще, мы могли бы применить его неперегруженную версию, возвращающую матрицу проекции. Но мы применили перегруз метода, который имеет ссылку (reference) и выходные параметры, что быстрее, т.к. при этом нет необходимости копировать текущие значения параметров.

Вид (View)

Теперь, когда мы установили матрицу проекции, мы можем установить матрицу вида (view matrix). В то время как проекцию (projection) можно представить в виде комбинации линз и фильтров объектива виртуальной камеры, вид (view) - это то, что эта камера "видит". Матрица вида содержит информацию о том, где у камеры верх, куда она направлена, а также её текущую позицию в 3D-пространстве. Для установки матрицы вида применим специальный вспомогательный метод из библиотеки XNA - Matrix.CreateLookAt, имеющий 3 параметра.
Вернёмся к началу Game1.cs, который после добавления двух новых объявлений выглядит так:
Фрагмент файла Game1.cs
...
namespace XNADemo
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

      private Matrix projection;
      private Matrix view;
...


  • Сразу после строки private Matrix view; добавь ниже следующие строки, объявляющие закрытые члены игрового класса:
private Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 3.0f);
private Vector3 cameraTarget = Vector3.Zero;
private Vector3 cameraUpVector = Vector3.Up;

  • Сохрани Решение (File -> Save All).

Теперь мы можем вызывать метод CreateLookAt прямо из метода InitializeCamera. Рассмотрим реализацию метода CreateLookAt, расположенного внутри метода InitializeCamera:
Фрагмент файла Game1.cs
...
            //View
            Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget,
                ref cameraUpVector, out view);
        }
...

Первый параметр - положение камеры. Как следует из трёх последних объявлений в самом начале Game1.cs, мы передаём координаты (0,0,3) в качестве начальной позиции камеры. Это означает, что камера будет расположена в точке отсчёта координатных осей X и Y, но будет сдвинута от него на 3 единицы (units).
Второй параметр - цель (target), куда направлена камера. В нашем случае камера направлена в начало координат (Vector3.Zero).
Третий параметр - вектор "верха" камеры. В нашем случае верх камеры направлен в положительном направлении оси Y (Vector3.Up). Обрати внимание, что для трёх первых параметров мы создали переменные. И теперь можем передавать их значения в виде ссылок (ref). Метод CreateLookAt также является перегруженным (overloaded). И потому он достаточно быстр, т.к. мы передаём значения в виде ссылок, а не конкретных значений. К счастью, читабельность исходного кода при этом почти не пострадала.


Image


Мир (World)

На данном этапе Решение и входящий в него Проект XNADemo можно скомпилировать, нажав F5 на клавиатуре или кнопку с зелёным треугольником на панели управления. В результате на экране появится всё то же окно с голубым фоном. А всё из-за того, что мы пока не установили мировую матрицу (world matrix) и не разместили в этом мире каких-либо объектов. Сейчас мы это быстро исправим.
Как ты мог видеть, шаблоны XNA предлагают множество различных методов для решения самых разных задач. Один из таких методов - Draw, реализация которго размещена в самом конце листинга Game1.cs:
Фрагмент файла Game1.cs
...
        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            base.Draw(gameTime);
        }

  • ПЕРЕД строкой base.Draw(gameTime); добавь следующую строку:
Matrix world = Matrix.Identity;

Здесь в качестве мировой матрицы устанавливается (присваивается) единичная (identity) матрица, что означает никакого масштабирования, вращения или трансляции (=перемещения). Единичная матрица имеет трансцляцию (0,0,0), что устанавливает нашу мировую матрицу в точке начала координат (origin) мира.
С этого момента у нас есть полностью настроенная (виртуальная) камера, но мы пока так ничего и не нарисовали.

Вершинные буферы (Vertex Buffers)

3D объекты состоят из треугольников (полигонов).
Закрыть
noteПолигоны и грани. В чём отличие?

Обрати внимание, что в классической геометрии грань фигуры может состоять из более чем трёх вершин (например гранью куба является квадрат, у которого 4 вершины). В терминологии 3D-моделирования принято приравнивать грань фигуры к полигону, который в большинстве систем состоит ровно из трёх вершин. Системы, оперирующие четырёхугольными полигонами встречаются нечасто. Одной из таких была игровая консоль Sega Saturn, увидевшая свет в 1995 г. Её создатели хотели как лучше, а на деле лишь прибавили головной боли разработчикам игр, которые ранее никогда не работали с четырёхугольными полигонами.
На Рис. 1 видно, что боковая грань куба на самом деле состоит из двух треугольных граней (полигонов). Мы также будем придерживаться этого правила, разбивая грани фигур, лежащие в одной плоскости и имеющие более чем 3 вершины, на полигональные треугольники.


Рис.4 Сфера состоит из треугольников
Рис.4 Сфера состоит из треугольников

Рис.5 Различия между 2-мя типами Картезианской системы координат
Рис.5 Различия между 2-мя типами Картезианской системы координат


Каждый объект представляет собой один или несколько треугольников. Например сфера состоит из треугольников. И чем их больше, тем более округлой она выглядит (См. Рис.4). Каждый треугольник состоии из трёх вершин. Для хранения этих вершин применяется вершинный буфер (vertex buffer), представляющий собой кусок памяти (=буфер), хранящий список вершин.
XNA использует праворучную (right-handed) систему координат (См. Рис.5). В ней положительный луч оси X исходит слева направо, ось Y устремляется снизу вверх, а ось Z направлена из точки начала координат к наблюдателю (камере).
XNA использует т.н. отсечение против часовой стрелки (counterclockwise culling) (См. Рис.4) Отсечение (culling, куллинг) представляет собой специальный приём, предотвращающий рендеринг объектов сцены, которые не повёнуты "лицом" (not facing) к камере. Это заметно повышает производительность 3D-рендеринга. В XNA есть 3 опции куллинга:
  • CullClockwiseFace,
  • CullCounterClockwiseFace (используется по умолчанию),
  • None.
Т.к. опция CullCounterClockwiseFace используется по умолчанию, то чтобы увидеть наш объект без лишних настроек, достаточно установить (set up) его вершины в порядке, противоположном движению часовой стрелки.
Закрыть
noteСовет

Начинающие игрокодеры нередко рисуют вершины (точки) на милиметровой бумаге или в обычном бумажном блокноте. При этом следует убедиться, что в коде они будут появляться в порядке, противоположном движению часовой стрелки.

Разместим несколько точек в коде Проекта XNADemo. В идеале должен получиться квадрат. Мы знаем, что все 3D-объекты состоят из треугольников, а квадрат, в свою очередь, из двух треугольников (См. Рис.1).
Мы разместим первый треугольник на след. координатах: (-1,1,0); (1,-1,0); (-1,-1,0). Это означает, что первая точка (=вершина) с координатами (-1,1,0) будет размещена: по оси X левее начала координат на 1 единицу, приподнята по оси Y на 1 единицу.

Объявление структуры VertexPositionNormalTexture[]

  • В открытом Проекте XNADemo в самом начале листинга Game1.cs найди следующий фрагмент:
Фрагмент файла Game1.cs
...
namespace XNADemo
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        private Matrix projection;
        private Matrix view;

        private Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 3.0f);
        private Vector3 cameraTarget = Vector3.Zero;
        private Vector3 cameraUpVector = Vector3.Up;
...


  • Сразу после строки private Vector3 cameraUpVector = Vector3.Up; добавь ниже следующие строки:
private VertexPositionNormalTexture[] vertices;
private Texture2D texture;

Реализация функции InitializeVertices, размещающей 3 вершины в 3D-пространстве

  • В листинге Game1.cs найди след. фрагмент с реализацией функции InitializeCamera, где мы инициализировали камеру наблюдателя:
Фрагмент файла Game1.cs
...
        private void InitializeCamera()
        {
            //Projection
            float aspectRatio = (float)graphics.GraphicsDevice.Viewport.Width /
                (float)graphics.GraphicsDevice.Viewport.Height;
            Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio,
                0.0001f, 1000.0f, out projection);

            //View
            Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget,
                ref cameraUpVector, out view);
        }
...

  • Прямо под ней добавь реализацию функции InitializeVertices:
Фрагмент файла Game1.cs
...
        private void InitializeVertices()
        {
            Vector3 position;
            Vector2 textureCoordinates;

            vertices = new VertexPositionNormalTexture[4];

            //top left
            position = new Vector3(-1, 1, 0);
            textureCoordinates = new Vector2(0, 0);
            vertices[0] = new VertexPositionNormalTexture(position, Vector3.Forward,
                textureCoordinates);

            //bottom right
            position = new Vector3(1, -1, 0);
            textureCoordinates = new Vector2(1, 1);
            vertices[1] = new VertexPositionNormalTexture(position, Vector3.Forward,
                textureCoordinates);

            //bottom left
            position = new Vector3(-1, -1, 0);
            textureCoordinates = new Vector2(0, 1);
            vertices[2] = new VertexPositionNormalTexture(position, Vector3.Forward,
                textureCoordinates);

            //top right
            position = new Vector3(1, 1, 0);
            textureCoordinates = new Vector2(1, 0);
            vertices[3] = new VertexPositionNormalTexture(position, Vector3.Forward,
                textureCoordinates);

        }
...

Реализация функции InitializeVertices начинается с объявления двух внутренних переменных:
Vector3 position;
Vector2 textureCoordinates;
В XNA есть разные структуры, описывающие типы данных для хранения векторов. В большинстве случаев (в любой типичной 3D-игре) необходимо сохранять данные о положении (position), нормалях (normal) и текстурные координаты. Нормали мы обсудим позднее. Сейчас важно знать о них то, что они позволяют графическому устройству определять, как именно будет отражаться свет от поверхности (=граней) 3D-объектов. Самая важная инфа для любой вершины - это её положение (position) в 3D-пространстве.

Источники:


1. Chad Carter. Microsoft XNA Game Studio 3.0 Unleashed. - Sams Publishing. 2009


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Знакомство с XNA Framework и XNA Game Studio  »  XNA - Создание 3D-объектов

Contributors to this page: slymentat .
Последнее изменение страницы Воскресенье 14 / Апрель, 2019 22:48:28 MSK автор slymentat.

Хостинг

Помочь проекту

Яндекс-деньги: 410011791055108