Данный материал взят с сайта old.shatalov.su и является его зеркалом

Создаём компьютерную игру. Создание игр на C++/DirectX

Есть вопросы?
Ошибка на сайте?
рус eng esp
Внимание! Данный сайт не обновляется. Новая версия: shatalov.su

Виртуальная камера. Реализация. Часть вторая

Дата создания: 2009-12-14 19:07:15
Последний раз редактировалось: 2012-02-08 10:49:07


Код программы, рассматриваемой в данном уроке, можно найти в разделе Листинги и программы

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

Начнём с описания программы: в программе камера можно передвигаться с помощью клавиш w,s,a,d,c,пробел; менять ориентацию в пространстве с помощью мышки и клавиш q,e. Кроме того, я немножко усложнил программу и теперь можно менять объекты управления: клавиша 1 (на основной клавиатуре) - управление камерой, клавиша 2 - управление пирамидой, клавиша 3 - управление кубом.

Для управления камерой и объектами (куб и пирамида) используются разные классы.

Продолжаем разбирать файл classes.h. Нам осталось рассмотреть классы MovableObject (перемещаемый объект) и Camera. Первый класс представляет объект (точнее, координатное пространство объекта), а второй - камеру (точнее, координатное пространство камеры). Сначала рассмотрим MovableObject, но прежде:

Ориентация в пространстве

С помощью ориентации объекта в пространстве можно ввести такие понятия для объекта как: лево, право, верх, низ, перед, зад.

Так как все объекты (в том числе и камера) имеют своё локальное координатное пространство, то ориентацию этих объектов удобно определять с помощью базисных векторов.

Так сложилось, что в программировании трёхмерной графики для задания величины поворота вокруг осей используются авиационные и морские термины: тангаж (pitch) - поворот вокруг оси x, рыскание (yaw) - поворот вокруг оси y, крен (roll) - поворот вокруг оси z. Второй популярный вариант именования углов вращения: наклон (pitch), поворот (heading) и крен (bank). Мы будем пользоваться вторым вариантом - именно так будут называться наши функции.

Здесь стоит сделать небольшое замечание по поводу слова наклон. В русском языке под данным словом можно понимать вращение в различных навправлениях: наклон влево/вправо, наклон вперёд/назад. Мы всегда будем использовать слово наклон (pitch) для обозначения вращения вокруг оси x: наклон назад (смотреть вверх), наклон вперёд (смотреть вниз).

Крен (bank) - вращение вокруг оси z: крен влево, крен вправо.

Поворот (heading) - вращение вокруг оси y: поворот налево, поворот направо.

Теперь, когда мы определилилсь с терминологией, вернёмся к классам MovableObject и Camera.

Класс MovableOjbect хранит базисные векторы локального пространства: i,j,k. Под локальным пространством здесь можно понимать координатное пространство любого объекта (только не камеры!). Помимо базисных векторов в классе присутствует вектор v - вектор начинающийся в начале мирового пространства и заканчивающийся в начале локального пространства. Компоненты этого вектора - четвёртая строка матрицы преобразования:

private:
Vector3 i,j,k,v;

Конструктор класса MovableObject инициализирует векторы i,j,k,v таким образом, чтобы локальное пространство совпадало с мировым:

MovableObject () : i(1,0,0), j(0,1,0), k(0,0,1), v(0,0,0) {}

Далее идут три метода задающие ориентацию объекта в пространстве: Heading (поворот), Bank (крен), Pitch (наклон). Каждый из этих методов принимает один аргумент - угол, на который нужно вращать объект:

void Heading(float angle)
{
  Matrix rotationMatrix;
  angle = angle*3.14f/180;
  rotationMatrix.RotationAroundAxis(Vector3(0,1,0),angle);
  multiplication(i,rotationMatrix);
  multiplication(k,rotationMatrix);
}

Внутри метода создаётся вспомогательная матрица rotationMatrix. Обратите внимание: мы подразумеваем, что в метод будет передана градусная величина угла, поэтому мы преобразовываем её в радианную.

Напоминаю, что матрица rotationMatrix - единичная (создаётся конструктором). Далее происходит вызов метода RotationAroundMatrix. Этот метод мы обсуждали в предыдущей части урока. Обратите внимание на первый аргумент данного метода - вызывается конструктора Vector3 - в метод будет передан базисный вектор j мирового координатного пространства. Это очень важный момент.

Почему мы передаём базисный вектор j мирового координатного пространства (0,1,0), а не базисный вектор j локального пространства (j.x,j.y,j.z)? Ответить на этот вопрос нам поможет простой пример и наше воображение: наклоните какой-нибудь объект на 90 градусов вперёд. Теперь вектор j этого объекта совпадает со старым местоположением вектора k. Теперь в этом положении поверните объект вокруг вектора j локального пространства. Сравните это вращение с поворотом вокруг вектора j мирового пространства. Вращение вокруг вектора j локального пространства можно использовать, например, в такой игре как descent или любом авиасимуляторе. Вращение же вокруг вектора j мировго пространства используется во всех стрелялках.

После вызова метода RotationAroundAxis в rotationMatrix находится матрица вращения вокруг базисного вектора j мирового координатного пространства на угол angle.

Напоминаю, что мы сейчас осуществляем поворот (heading, вращение вокруг оси j). Какие векторы нужно изменить для этого (правильный ответ - в последнем уроке по преобразованиям)? ... Ну хорошо, я подскажу: векторы i и k. Для этого нужно перемножить эти векторы на матрицу rotationMatrix.

Методы Bank и Pitch очень похожи на Heading:

void Bank(float angle)
{
  Matrix rotationMatrix;
  angle = angle*3.14f/180;
  rotationMatrix.RotationAroundAxis(k,angle);
  multiplication(i,rotationMatrix);
  multiplication(j,rotationMatrix);	
}

void Pitch(float angle)
{
  Matrix rotationMatrix;
  angle = angle*3.14f/180;
  rotationMatrix.RotationAroundAxis(Vector3(i.x,0,i.z),angle);
  multiplication(j,rotationMatrix);
  multiplication(k,rotationMatrix);
 }

Отличие - вектор, вокруг которого происходит вращение. В случае крена - это вектор k. В случае наклона - вектор Vector3(i.x,0,i.z), т.е вектор i с компонентой y равной нулю.

Следующие три метода перемещают объект в пространстве: Strafe (движение влево, вправо), Fly (движение вверх, вниз), Walk (движение вперёд, назад):

void Strafe(float dx) { v += Vector3(i.x,0,i.z) * dx; }
void Fly (float dy)   { v += Vector3(0,j.y,0) * dy; }
void Walk (float dz)  { v += Vector3(k.x,0,k.z) * dz; }

Как работают эти формулы можно узнать в последнем уроке по преобразованиям.

Последний метод CreateMatWorld создаёт матрицу преобразования из локального пространства в мировое:

void CreateMatWorld(Matrix& matCam)
{
  k.Normalize();
  j = k.Cross(i);
  j.Normalize();
  i = j.Cross(k);
  i.Normalize();
  matCam._11 = i.x;
  matCam._12 = i.y;
  matCam._13 = i.z;
  matCam._21 = j.x;
  matCam._22 = j.y;
  matCam._23 = j.z;
  matCam._31 = k.x;
  matCam._32 = k.y;
  matCam._33 = k.z;
  matCam._41 = v.x;
  matCam._42 = v.y;
  matCam._43 = v.z;
}

Разберём первые пять строк. Базисные векторы пространства должны быть единичной длины и перпендикулярны друг другу. Во время наклона, крена, поворота увеличивается погрешность в вычислениях: векторы i,j,k перестают быть перпендикулярными, а их длина перестаёт быть равной единице. Метод Vector3::Normalize нормирует вектор (делает его длину равной единице). Метод Cross - векторное произведение векторов. Я упустил этот момент и мы его не рассмотрели, но векторное произведение векторов даёт вектор, перпендикулярный векторам участвовавшим в произведении. После выполнения пяти операторов, векторы i,j,k снова перпендикулярны друг другу, а их длины равны единице.

Оставшаяся часть кода должна быть понятна: происходит заполнение матрицы преобразования.

Сейчас ещё раз посмотрите на класс MovableObject. Вам должно быть понятно абсолютно всё в этом классе. Если это не так, то читайте уроки по преобразованиям. Если после прочтения всех уроков по преобразованиям и предыдущих выпусков, вам всё равно не понятны какие-то части данного класса, то пожалуйста (очень прошу) напишите мне на email и укажите, что именно непонятно.

Теперь класс Camera. Данный класс имеет очень много общего с классом MovableObject. Мы сосредоточимся только на отличиях. Методы Pitch, Heading, Fly, Walk, Strafe полностью совпадают с соответствующими методами класса MovableObject.

Первое отличие - в классе присутствует переменная angleAroundZ. С помощью данной переменной отслеживается угол вращения вокруг оси z.

Метод Bank

void Bank(float angle)
{
  if (angleAroundZ < 45 && angleAroundZ > -45)
    angleAroundZ += angle;
  if ((angleAroundZ >= 45 && angle > 0) || (angleAroundZ <= -45 && angle < 0))
  {
    angleAroundZ -= angle;
    return;
  }
  Matrix RotationMatrix;
  angle = angle*3.14f/180;
  RotationMatrix.RotationAroundAxis(k,angle);
  multiplication(i,RotationMatrix);
  multiplication(j,RotationMatrix);
}

В данной реализации класса камеру можно кренить только на 45 градусов. При вызове метода Bank мы проверяем переменную angleAroundZ. Значение данной переменной должно находиться в диапазоне от -45 до 45. Если это условие выполняется, то происходит изменение этой переменной на угол angle.

Второй оператор if не даёт выйти переменной angleAroundZ за допустимый диапазон: проверяется значение переменной angleAroundZ и направление вращения (угол angle).

Оставшаяся часть кода совпадает с методом MovableObject::Bank.

Следующий метод предназначен для плавного возвращения камеры в исходную позицию после того как пользователь отпустил клавишу крена:

void CheckBanking()
{
  if (angleAroundZ > 0)
  {
    float angle = -1;
    Matrix RotationMatrix;
    angle = angle*3.14f/180;

    RotationMatrix.RotationAroundAxis(Vector3(k.x,k.y,k.z),angle);
    multiplication(i,RotationMatrix);
    multiplication(j,RotationMatrix);
    --angleAroundZ;
  }
  if (angleAroundZ < 0)
  {
    float angle = 1;
    Matrix RotationMatrix;
    angle = angle*3.14f/180;

    RotationMatrix.RotationAroundAxis(Vector3(k.x,k.y,k.z),angle);
    multiplication(i,RotationMatrix);
    multiplication(j,RotationMatrix);
    ++angleAroundZ;
  }
}

Каждый раз, когда вызывается данная функция, крен будет изменяться на один градус в противоположную сторону от текущего значения крена.

У данного метода есть одна слабость. Чем быстрее компьютер пользователя, тем быстрее камера будет возвращаться в исходную позицию. Правильнее было бы сделать возвращение в исходную позицию в зависимости от времени. Но пока удовлетворимся тем что есть.

И последний метод - CreateMatCam:

void CreateMatCam(Matrix& matCam)
{
  k.Normalize();
  j = k.Cross(i);
  j.Normalize();
  i = j.Cross(k);
  i.Normalize();
  matCam._11 = i.x;
  matCam._12 = j.x;
  matCam._13 = k.x;
  matCam._21 = i.y;
  matCam._22 = j.y;
  matCam._23 = k.y;
  matCam._31 = i.z;
  matCam._32 = j.z;
  matCam._33 = k.z;
  matCam._41 = -(i*v);
  matCam._42 = -(j*v);
  matCam._43 = -(k*v);
}

Здесь присутствует важный момент, который мы ещё не рассмотрели. Хотел про него написать в последнем уроке по преобразованиям, но руки так и не дошли. Поэтому кратко.

При преобразовании из мирового пространства в пространство камеры нужно поступать наоборот: преобразовывать координаты из пространства камеры в мировые. Это можно сделать с помощью следующей матрицы:

  i.x    j.x    k.x  0
  i.y    j.y    k.y  0
  i.z    j.z    k.z  0
-(i*v) -(j*v) -(k*v) 1

Почему используется именно такая матрица объясню в последнем уроке по преобразованиям... Как только найду время.

По файлу classes.h всё.

Теперь рассмотрим файл, в котором определена функция WinMain.

В программе камера для управления разными типами объектов используются функции. Для управления камерой используется функция ChangeCamera (изменить камеру), а для управления всеми остальными объектами используется ChangeModel (изменить модель). Заголовки этих функций одинаковы.

Так как в любой момент времени можно управлять только одним объектом, то мы воспользуемся указателем на функцию.

void (*ChangeObject)(float angleAroundX, float angleAroundY, float angleAroundZ,char buffer[]);
void ChangeModel (float angleAroundX, float angleAroundY, float angleAroundZ,char buffer[]);
void ChangeCamera (float angleAroundX, float angleAroundY, float angleAroundZ,char buffer[]);

Сразу рассмотрим определения ChangeModel и ChangeCamera (они расположены в конце файла, после WinMain). Напоминаю, что у указателя на функцию нет определения.

void ChangeModel (float angleAroundX, float angleAroundY, float angleAroundZ,char buffer[])
{
if (buffer[DIK_D] & 0x80) 
  object->Strafe(1);
else if(buffer[DIK_A] & 0x80) 
  object->Strafe(-1);
if (buffer[DIK_SPACE] & 0x80)
  object->Fly(0.5);
if (buffer[DIK_C] & 0x80)
  object->Fly(-0.5);
if (buffer[DIK_W] & 0x80)
  object->Walk(1);
if (buffer[DIK_S] & 0x80)
  object->Walk(-1);

if (angleAroundY != 0)
  object->Heading(angleAroundY);
if (angleAroundX != 0)
  object->Pitch(angleAroundX);
if (angleAroundZ != 0)
  object->Bank(angleAroundZ);	
}

В функцию передаётся три переменных: angleAroundX (вращение вокруг X), angleAroundY (вращение вокруг Y), angleAroundZ (вращение вокруг Z) и массив buffer. Из названий переменных легко догадаться о их назначении, а массив buffer - это состояние клавиатуры, в нём 256 элементов.

В теле функции проверяется какие клавиши клавиатуры были нажаты: d,a,пробел,c,w,s и вызываются соответствующие методы текущего объекта.

Затем проверяются углы вращения и вызываются соответствующие методы.

Теперь функция ChangeObject. Она немножко сложнее:

void ChangeCamera (float angleAroundX, float angleAroundY, float angleAroundZ,char buffer[])
{
if (buffer[DIK_D] & 0x80) 
  cam.Strafe(1);
else if(buffer[DIK_A] & 0x80) 
  cam.Strafe(-1);
if (buffer[DIK_SPACE] & 0x80)
  cam.Fly(0.5);
if (buffer[DIK_C] & 0x80)
  cam.Fly(-0.5);
if (buffer[DIK_W] & 0x80)
  cam.Walk(1);
if (buffer[DIK_S] & 0x80)
  cam.Walk(-1);

if (angleAroundY != 0)
  cam.Heading(angleAroundY);
if (angleAroundX != 0)
  cam.Pitch(angleAroundX);
if (angleAroundZ != 0)
  cam.Bank(angleAroundZ);

if (buffer[DIK_Q] & 0x80)
  cam.Bank(5);
else if (buffer[DIK_E] & 0x80)
  cam.Bank(-5);
else
  cam.CheckBanking();
}

Здесь присутствует весь код из предыдущей функции и проверка нажатия клавиш q и e. Обратите внимание, что если не была нажата ни одна из этих двух клавиш, то вызывается метод CheckBanking, который медленно (один градус за раз) возвращает камеру на место после крена.

Теперь возвращаемся в начало файла и смотрим на объявление глобальных переменных:

HWND hWnd;
IDirect3D9* d3d = NULL;
IDirect3DDevice9* videocard = NULL;
IDirect3DVertexBuffer9* vbCube = NULL;
IDirect3DVertexBuffer9* vbFrustum = NULL;
IDirect3DVertexBuffer9* vbAxises = NULL;
IDirect3DIndexBuffer9* ibGeometry = NULL;
IDirectInput8* di = NULL;
IDirectInputDevice8* mouse = NULL;
IDirectInputDevice8* keyboard = NULL;

Camera cam;
MovableObject Cube;
MovableObject Frustum;
MovableObject* object = NULL;

В программе мы используем три различных устройства: одно Direct3D (видеокарта) и два устройства DirectInput (мышь и клавиатура). Как вы помните, в предыдущих программах мы использовали переменную dev (от Device - устройство) для представления видеокарты. В этой программе я поменял имя этой переменной. Теперь у каждого устройства более подходящее имя: videocard, mouse, keyboard.

В программе присутствует три объекта: куб, усечённая пирамида и оси мирового пространства (ещё конечно же камера, но для неё не нужно выделять ресурсы на этом этапе). Мы воспользуемся тремя вершинными буферами: vbCube, vbFrustum (эмм... усечённая пирамида), vbAxises (оси). В идентификаторах присутствуют буквы vb - от vertex buffer (вершинный буфер). Оси координат будут выводиться полностью через вершинный буфер, а вот для куба и пирамиды мы создадим индексный буфер. Причём, будет использоваться один буфер для индексов и пирамиды и куба - ibGeometry. ib - index buffer (индексный буфер).

Далее мы создаём один объект класса Camera и два объекта класса MovableObject: Cube и Frustum. Так как в какой-то определённый момент времени мы можем управлять только одним объектом (камерой, кубом, пирамидой), то нам понадобится указатель на MovableObject, в котором мы будем хранить объект, которым можно управлять в данный момент.

Функция WinMain. Инициализация

Нам понадобится три вершинных буфера и один индексный:

videocard->CreateVertexBuffer( 8*sizeof(Vertex), D3DUSAGE_WRITEONLY,
                         D3DFVF_XYZ|D3DFVF_DIFFUSE, D3DPOOL_DEFAULT,
                         &vbCube,NULL);
videocard->CreateVertexBuffer( 8* sizeof(Vertex), D3DUSAGE_WRITEONLY,
                         D3DFVF_XYZ|D3DFVF_DIFFUSE, D3DPOOL_DEFAULT,
                         &vbFrustum,NULL);
videocard->CreateVertexBuffer( 6* sizeof(Vertex), D3DUSAGE_WRITEONLY,
                         D3DFVF_XYZ|D3DFVF_DIFFUSE, D3DPOOL_DEFAULT,
                         &vbAxises,NULL);
videocard->CreateIndexBuffer( 60*sizeof(unsigned short),
                        D3DUSAGE_WRITEONLY, D3DFMT_INDEX16,
                        D3DPOOL_DEFAULT, &ibGeometry, NULL );

Дальше устанавливаем состояния рендеринга:

videocard->SetRenderState(D3DRS_LIGHTING, false);
videocard->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
videocard->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);

Теперь нужно проинициализировать объекты сцены. Начнём с куба.

Первое: нужно создать восемь вершин - объектов класса Vertex. Именно эти вершины будут скопированы в вершинный буфер vbCube. Здесь же мы инициализируем все вершины цветом 0xff555555, а две противоположные вершины разными цветами.

Второе: создание массива из восьми объектов Vector4. Именно над элементами этого массива будут производиться преобразования. После преобразования три компоненты вектора будут скопированы в массив cube.

Третье: массив индексов. Элементы этого массива будут скопированы в индексный буфер ibGeometry:

Vertex cube[8];
for (counter = 0; counter < 8; counter++)
  cube[counter].color = 0xff555555;
cube[0].color = 0xffff5555;
cube[7].color = 0xff55ff55;
Vector4 cubeCoords[8] = 
{
  Vector4(-1,-1,-1,1),
  Vector4(-1,1,-1,1),
  Vector4(1,-1,-1,1),
  Vector4(1,1,-1,1),
  Vector4(-1,-1,1,1),
  Vector4(-1,1,1,1),
  Vector4(1,-1,1,1),
  Vector4(1,1,1,1)
};
unsigned short cubeIndexes[36] =
{
  0, 1, 2,
  1, 3, 2,
  0, 1, 5,
  0, 4, 5,
  4, 5, 6,
  5, 7, 6,
  2, 3, 7,
  2, 6, 7,
  0, 2, 6,
  0, 4, 6,
  1, 3, 7,
  1, 5, 7,
};

Обратите внимание на именование переменных:
Cube - переменная типа MovableObject начинается с заглавной буквы.
vbCube - вершинный буфер начинается с vb.
cube - массив из элементов типа Vertex начинается с маленькой буквы.
cubeCoords - массив элементов типа Vector4 имеет постфикс Coords.
cubeIndexes - массив содержащий индексы объекта имеет постфикс Indexes.
matCube - матрица локального пространства куба имеет префикс mat.

Данные правила распространяются и на другие объекты сцены (оси и пирамиду).

Усечённая пирамида:

Vertex frustum[8];
for (counter = 0; counter < 8; ++counter)
  frustum[counter].color = 0xff000000;
Vector4 frustumCoords[] = {
  Vector4(-10,-10,30,1),
  Vector4(10,-10,30,1),
  Vector4(-10,10,30,1),
  Vector4(10,10,30,1),
  Vector4(-1,-1,10,1),
  Vector4(1,-1,10,1),
  Vector4(-1,1,10,1),
  Vector4(1,1,10,1),
};
unsigned short frustumIndexes[24] =
{
  0, 1,
  1, 3,
  3, 2,
  2, 0,
  4, 5,
  7, 6,
  6, 4,
  5, 7,
  0, 4,
  2, 6,
  3, 7,
  1, 5 
};

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

Оси:

Vertex axises[6];
axises[0].color = 0xffaa0000;
axises[1].color = 0xffaa0000;
axises[2].color = 0xff00aa00;
axises[3].color = 0xff00aa00;
axises[4].color = 0xff0000aa;
axises[5].color = 0xff0000aa;
Vector4 axisesCoords[6] = 
{
  Vector4(0,0,0,1),
  Vector4(5,0,0,1),
  Vector4(0,0,0,1),
  Vector4(0,5,0,1),
  Vector4(0,0,0,1),
  Vector4(0,0,5,1)
};

Оси мирового пространства имеют длину в пять единиц и окрашены в разные цвета: x - красный, y - зелёный, z - синий. Для вывода осей на экран используется только вершинный буфер.

Теперь нужно разместить объекты в пространстве и задать проекционную матрицу:

Matrix matProj;
matProj._33=1000/999;
matProj._43=-1000/999;
matProj._34=1;
matProj._44=0;

Matrix matCam;
cam.Strafe(-40); // влево на 40
cam.Fly(15);     // вверх на 15
cam.Walk(-35);   // назад на 35
cam.Pitch(30);   // наклон назад на 30 градусов
cam.Heading(30); // поворот направо на 30 градусов

Matrix matAxises;

Matrix matCube;
Cube.Fly(-10);    // вниз на 10
Cube.Strafe(-15); // влево на 15
Cube.Walk(25);    // вперёд на 25

Matrix matFrustum;
Frustum.Heading(45);  // поворот направо на 45 градусов
Frustum.Pitch(90);    // наклон назад на 90 градусов
Frustum.Fly(25);      // вверх на 25
Frustum.Strafe(20);   // вправо на 20
Frustum.Walk(-20);    // назад на 20

Обратите внимание, что местоположение объектов мы задаём с помощью соответствующих классов. Оцените насколько проще стало размещать объекты в пространстве!!! Главное, запомнить перевод неизвестных слов и не путать в какую сторону происходит положительное вращение, а в какую отрицательное.

Оси мирового пространства координат должны оставаться неподвижными, поэтому мы и не трогаем соответствующую единичную матрицу.

Дальше идёт создание вспомогательных переменных:

MSG msg;
void* vb = NULL;
void* ib = NULL;
HRESULT  hr = NULL;
DIMOUSESTATE dims;
ZeroMemory(&dims,sizeof(dims));
char buffer[256]; // Буфер. хранит состояние клваиатуры.
ZeroMemory(buffer,sizeof(buffer));
float angleAroundX = 0, angleAroundY = 0, angleAroundZ = 0;

Тут нужно не забыть обнулить структурную переменную dims и массив buffer, в которые будут сохраняться состояния мыши и клавиатуры.

Из новых переменных - углы поворота вокруг различных осей.

До начала основного цикла можно заполнить индексный буффер - его содержимое не будет меняться во время работы основного цикла:

ibGeometry->Lock(0,sizeof(cubeIndexes)+sizeof(frustumIndexes),(void**)&ib,0);
memcpy(ib,cubeIndexes,sizeof(cubeIndexes));
ib = static_cast<unsigned short*>(ib) + 36;
memcpy(ib,frustumIndexes,sizeof(frustumIndexes));
ibGeometry->Unlock();

Замыкаем буфер, копируем индексы куба, затем смещаемся в буфере на длину массива индексов куба (в нём 36 индексов), копируем в индексный буфер индексы пирамиды и, наконец, открываем буфер.

Последняя строчка перед основным циклом:

ChangeObject = ChangeCamera;

При запуске программы нужно управлять камерой, поэтому мы присваиваем указателю на функцию ChangeObject функцию ChangeCamera.

Основной цикл

В основном цикле присутствует код для приобретения контроля над устройством в случае потери:

hr = keyboard->GetDeviceState(sizeof(buffer),buffer);
if (hr != DI_OK) 
  keyboard->Acquire(); 
hr = mouse->GetDeviceState(sizeof(DIMOUSESTATE),&dims);
if (hr != DI_OK)
  mouse->Acquire();

Лучше же конечно сделать бесконечный цикл, но пока и так сойдёт. Дальше ввод с клавиатуры и мышки:

if (dims.lX > 0)
  angleAroundY = 5;;
if (dims.lX < 0)
  angleAroundY = -5;
if (dims.lY > 0)
  angleAroundX = 5;
if (dims.lY < 0)
  angleAroundX = -5;

if (buffer[DIK_1] & 0x80)
  ChangeObject = ChangeCamera;
if (buffer[DIK_2] & 0x80)
{
  ChangeObject = ChangeModel;
  object = &Frustum;
}
if (buffer[DIK_3] & 0x80)
{
  ChangeObject = ChangeModel;
  object = &Cube;
}

Первая часть: проверка движения мышки. Даже если пользователь чуть-чуть двинул мышь, мы увеличиваем один из углов вращения на 5 градусов. В будущем мы поставим скорость движения камеры в зависимость от скорости движения мышки. Без этого говорить о хорошей реализации камеры не имеет смысла. Но пока, вот так.

Вторая часть: проверка ввода клавиш 1 (камера), 2 (пирамида), 3 (куб). В случае единицы, мы меняем только ChangeObject. В случае 2 или 3 помимо ChangeObject нужно поменять и объект

Далее вызывается функция ChangeObject:

ChangeObject(angleAroundX,angleAroundY,angleAroundZ,buffer);

Обнуляем углы вращения для следующей итерации цикла:

angleAroundY = 0;
angleAroundX = 0;
angleAroundZ = 0;

Теперь нужно создать матрицы преобразования:

cam.CreateMatCam(matCam);
Frustum.CreateMatWorld(matFrustum);
Cube.CreateMatWorld(matCube);

Далее нужно преобразовать все вершины сцены:

for (int i = 0; i < 8; i++)
  transformations(cube[i],cubeCoords[i],matCube,matCam,matProj);
for (int i = 0; i < 6; i++)
  transformations(axises[i],axisesCoords[i],matAxises,matCam,matProj);
for (int i = 0; i < 8; i++)
  transformations(frustum[i],frustumCoords[i],matFrustum,matCam,matProj);

Обратите внимание, что для разных объектов функция transformations вызывается с соответствующей матрицей преобразования из объектного пространства в мировое.

Теперь, когда вершины преобразованы и помещены в соответствующие массивы (cube,axises,frustum), нужно поместить их в вершинные буферы:

vbCube->Lock(0,sizeof(cube),(void**)&vb,0);
memcpy(vb,cube,sizeof(cube));
vbCube->Unlock();
vbFrustum->Lock(0,sizeof(frustum),(void**)&vb,0);
memcpy(vb,frustum,sizeof(frustum));
vbFrustum->Unlock();
vbAxises->Lock(0,sizeof(axises),(void**)&vb,0);
memcpy(vb,axises,sizeof(axises));
vbAxises->Unlock();

Это было просто!

Начинаем вывод:

videocard->BeginScene();
videocard->SetStreamSource(0,vbCube,0,sizeof(Vertex));
videocard->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0,8,0,12);
videocard->SetStreamSource(0,vbFrustum,0,sizeof(Vertex));
videocard->DrawIndexedPrimitive(D3DPT_LINELIST,0,0,8,36,12);

videocard->SetStreamSource(0,vbAxises,0,sizeof(Vertex));
videocard->DrawPrimitive(D3DPT_LINELIST,0,3);
videocard->EndScene();

Во время вывода мы трижды меняем источники геометрии для сцены с помощью SetStreamSource: сначала мы выводим куб, затем пирамиду и в конце оси.

Обратите внимание, что при выводе пирамиды в предпоследнем аргументе указывается смещение от начала буфера. Кроме того, в первом аргументе мы указываем, что будут выводиться отрезки а не треугольники.

Для вывода осей, мы также используем отрезки (D3DPT_LINELIST).

Вот и всё!!!

Заключение

Урок был длинным, программа - сложной. Скорее всего вам есть над чем подумать.

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

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

Упражнения

1. Исправьте ошибку в исходном коде. Ошибка - точно такая же, как и в программе Преобразования. Эту ошибку вы должны были исправить в одном из предыдущих уроков. :) Вы ведь её уже нашли, не так ли? Ну что ж, тогда на исправление у вас уйдёт меньше минуты.

2. В разделе Листинги и программы скачайте программу camera_1. На основе программы, которую мы разбирали в этом уроке, реализуйте функциональность программы camera_1. Особенности программы:

Первое. При нажатии на клавишу 2 на основной клавиатуре, камера перемещается в начало координат локального пространства пирамиды. Координаты пирамиды следующие:

Vector4 frustumCoords[] = {
  Vector4(-10,-10,30,1),
  Vector4(10,-10,30,1),
  Vector4(-10,10,30,1),
  Vector4(10,10,30,1),
  Vector4(-1,-1,10,1),
  Vector4(1,-1,10,1),
  Vector4(-1,1,10,1),
  Vector4(1,1,10,1),
};

Ориентация камеры совпадает с ориентацией пирамиды.

Второе. При нажатии на клавишу 2, перед тем как камера переместится на новое место, на старое местоположение камеры перемещается куб. В программе я размещал куб на 10 единиц впереди камеры.

Третье. При нажатии на клавишу 1 камера возвращается на свою старую позицию.

Четвёртое. В программе всего два режима, между которыми можно переключаться клавишами 1 и 2. Т.е. в camera_1 нельзя управлять кубом.

В своей реализации я добавил к классам MovableObject и Camera по два метода:

void SetBasisVectors(Vector3 x,Vector3 y,Vector3 z,Vector3 p)
{ i = x; j = y;	k = z; v = p; }
void GetBasisVectors(Vector3& x, Vector3& y, Vector3& z, Vector3& p)
{ x = i; y = j; z = k; p = v; }

Первый метод устанавливает базисные векторы объекта, второй позволяет их получить. Можете воспользоваться этими методами, а можете придумать что-то своё.