Домашняя страница
Обучение
Для абитуриентов
 
 
Для студентов
 
 
IT-English
Центры обучения
О нас
Статьи
Создание 3D объекта с использованием буфера вершин и индексов

Наверное, много программистов задумывалось над тем, чтобы написать свою игру, или хотя бы что-нибудь трехмерное (особенно, в студенческие годы). Однако не у всех было желание разбираться в запутанном DirectX, инициализировать Direct3D, запоминать длинные названия констант и т.д., и т.п. Теперь разработка приложений такого плана стала более доступна с выходом XNA Framework, который предоставляет нам легкий доступ к графическим устройствам и устройствам ввода (клавиатура, джойстик, мышь), контроль аудио потоков и многое другое.
(Здесь можно прочитать больше о самой технологии и процессе установки Game Studio)

В своей статье хотелось бы показать, как создать простой трехмерный объект (куб). Для этого, в принципе, много знаний не надо. Начнем, наверное, с того, из чего состоит игровое приложение и как вообще оно работает. Создаем новый Windows Game проект. Видим, что у нас много комментариев и шесть методов (кто умеет читать на английском, поймут, зачем эти методы нужны). Первым у нас идет конструктор. За ним метод Initialize (для какой-либо инициализации до старта игры); LoadGraphicsContent (для загрузки графики); (естественно для выгрузки графики);
И два метода, на которых и замешан весь процесс игры, – Update и Draw первый предназначен для логики, а второй для прорисовки игры. Логика самого приложения выглядит приблизительно так:
  • Вызов игрового конструктора.
  • Конструктор в свою очередь создает игровые компоненты и вызывает их конструкторы.
  • XNA Framework вызывает метод главного класса игры Initialize().
  • Этот же метод вызывается для каждого из компонентов.
  • Далее Фреймворк вызывает метод LoadGraphicsContent() для всех рисуемых компонентов игры.
  • LoadGraphicsContent() для главного класса игры.
  • Вызывается метод Update() для игры.
  • Потом метод Update() для всех компонентов игры.
  • Вызов метода Draw() для игры.
  • Метод Draw для всех рисуемых компонентов игры.
  • Шаги 7 – 10 повторяются с частотой несколько раз в секунду.
  • Если пользователь, например, свернул окно, то вызывается метод UnloadGraphicsContent().
  • Если окну опять вернулся фокус, то все начинается с этапа 6.
  • Игрок выходит из игры.
  • Вызов метода Dispose() для игры.
  • Фреймворк вызывает Dispose() для всех компонентов игры.
  • Вызов UnloadGraphicsContent().
  • Игра закрывается.

БУФЕР ВЕРШИН


Куб имеет 8 вершин. Сейчас мы заполним буфер координатами и, в дальнейшем используя их, нарисуем куб, но об этом чуть позже. Сейчас нам нужны только координаты в пространстве. В XNA используется правосторонняя система координат. Для того, чтобы представить, в каком направлении идут позитивные значения, выставьте свою правую руку так, будто вы держите пистолет. Теперь поверните кисть ладонью вверх. Указательный палец показывает в направлении позитивных значений оси Х, большой палец – оси Z, а ладонь – Y.

Буфером вершин в нашей программе будет обычный массив структур Vector3.
Объявим его перед конструктором Vector3 [] vertices; Теперь создадим метод, который будет заполнять наш массив. Координаты заданы так, что центр куба и начало системы координат совпадут.
private void InitializeVertices()
{
        vertices = new Vector3[8];

        vertices[0] = new Vector3(-1, 1, -1);
        vertices[0] = new Vector3(1, 1, -1);
        vertices[0] = new Vector3(1, -1, -1);
        vertices[0] = new Vector3(-1, -1, -1);
        vertices[0] = new Vector3(-1, 1, 1);
        vertices[0] = new Vector3(1, 1, 1);
        vertices[0] = new Vector3(1, 1, 1);
        vertices[0] = new Vector3(-1, -1, 1);
}

Добавляем вызов InitializeVertices() в Initialize. Наш буфер вершин готов.


БУФЕР ИНДЕКСОВ

Теперь мы приступим к созданию буфера индексов. Это будет обычный массив значений short. «Зачем он нам нужен?» - спросите Вы. Наша видеокарта рисует только точки, линии и треугольники. Нарисовать куб из линий или точек будет не очень просто, легче всего использовать треугольники, но для
того, чтобы их могла отобразить видеокарта, ей нужно знать как соединить вершины. Значения нашего буфера и будут инструкциями для видеоадаптера. Еще очень важная деталь - это указание последовательности соединения точек при построении треугольника, потому что по ней определяется, какая сторона будет видимой, а какая нет. Видимой будет сторона, точки которой рисуются по часовой стрелке. На рисунке мы видим, что оба треугольника первого квадрата построены по часовой стрелке, поэтому они будут видимы для нас. На втором же квадрате серый треугольник рисуется по часовой стрелке, соответственно он будет видимым, а второй – нет. Но если мы развернем его, то с другой стороны построение будет по часовой стрелке. Поэтому нужно быть внимательным, устанавливая индексы. Определим наш массив индексов сразу же после буфера вершин и создадим метод, который будет его инициализировать.
short[] indices;

private void InitializeIndecies()
{
        indices = new short[]
        {
            0,3,2,0,2,1,
            1,2,6,1,6,5,
            3,6,2,3,7,6,
            0,7,3,0,4,7,
            4,6,7,4,5,6,
            0,5,4,0,1,5
        };
}
Вызов этого метода тоже поместим в Initialize();


КАМЕРА

Теперь мы создадим камеру, с помощью которой будем смотреть на мир. Конечно, лучше было бы сделать камеру в отдельном компоненте, чтобы использовать этот класс и в других проектах, но это Вы сможете сделать и сами. Начнем создание камеры с объявления трех private полей.

Matrix world; Matrix projection; Matrix view;
Теперь напишем метод, который будет настраивать нашу камеру.
private void InitializeCamera()
{
            float AspectRatio = (float)graphics.GraphicsDevice.Viewport.Width / (float)graphics.GraphicsDevice.Viewport.Height;
            Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, AspectRatio, 0.0001f, 1000.0f, out projection);
            Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref cameraUpVector, out view);
}

Первое, что мы делаем, это находим значение переменной AspectRatio. Это наше форматное соотношение. Форматное соотношение = ширинаЭкрана / высотаЭкрана. При переносе из квадратной области окна проекции объектов на экран, который у нас прямоугольный, происходит искажение. Данная переменная будет использоваться для корректировки искажений. После того, как мы сделали вычисления, приступаем к созданию проекции. Проекция - это переход от n мерного пространства к (n-1) мерному (превращение нашего куба в двухмерное изображение на экране). В данном случае мы создаем перспективную проекцию, выполняющуюся таким образом, что объекты, находящиеся дальше от камеры выглядят меньше, чем того же размера, размещенные ближе.

Рассмотрим параметры метода:
    fieldOfView
        Угол обзора в радианах. Мы можем использовать следующею запись MathHelper.ToRadians(45.0f); (угол обзора в 45 градусов)
    aspectRatio
        Форматное соотношение.
    nearPlaneDistance
        Расстояние до ближней видимой точки.
    farPlaneDistance
        Расстояние до дальней видимой точки.
    result
        [выход] матрица projection.

Вы, наверное, заметили вызов Matrix.CreateLookAt, он устанавливает матрицу вида. Нам потребуется еще три поля для работы с этим методом.
    Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 15.0f);
    Vector3 cameraTarget = Vector3.Zero;
    Vector3 cameraUpVector = Vector3.Up;

Рассмотрим параметры CreateLookAt:
    cameraPosition
        позиция камеры
    cameraTarget
        точка, на которую смотрит наша камера
    cameraUpVector
        параметр, который устанавливает направление «вверх» для нашей сцены
    result
        [выход] Матрица view

У нас осталась только одна незаполненная матрица - world. В конструкторе добавим ее инициализацию, она достаточно проста:
    world = Matrix.Identity;

Как обычно вызываем новый метод в Initialize();
Все, камера готова. Осталось нарисовать наш “многострадальный” кубик.


DRAW

Рисование происходит в методе Draw(), туда мы и поместим код нашей прорисовки.

graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(graphics.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
BasicEffect effect = new BasicEffect(graphics.GraphicsDevice, null);

effect.World = world;
effect.Projection = projection;
effect.View = view;

effect.GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
effect.Begin();

foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
        pass.Begin();
        graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleTriangleList, vertices, 0, vertices.Length, indices, 0, indices.Length / 3);         pass.End();
}

effect.End();
world *= Matrix.CreateRotationY(0.05f) * Matrix.CreateRotationX(0.05f) * Matrix.CreateRotationZ(0.005f);

Теперь пошагово посмотрим, что происходит в методе Draw(). В XNA для показа чего-либо на экране используются эффекты. Мы видим создание новой переменной – BasicEffect. Следующий шаг – установка ее свойств. Строкой effect.GraphicsDevice.RenderState.FillMode = FillMode.WireFrame; мы говорим видеокарте рисовать только каркас нашего куба, так мы сможем увидеть, как именно он рисуется из треугольников. Заметьте, что наша прорисовка находится между двумя методами Begin() и End(). Ко всему, что мы вырисовываем между этими двумя вызовами, будет добавлен эффект. О том, что именно происходит в этом цикле, мы напишем в следующей статье.

Рассмотрим только метод DrawUserIndexedPrimitives
    примитивы, с помощью которых мы будем рисовать,
    массив вершин,
    с какой вершины начинать,
    количество вершин,
    массив индексов,
    с какого индекса начинать,
    общее количество примитивов, которое надо отрисовать.

Последняя строчка - это операция, которая позволит нам вращать куб.

Автор: Дмитрий Охрименко