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

Прежде чем мы приступим к рассмотрению кода программы, необходимо решить некоторые организационные вопросы.

Соглашение по именованию переменных

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

Начнём с простого правила: все имена классов и их методов начинаются с большой буквы. Каждое новое слово в идентификаторе также начинается с заглавной буквы. Между словами никаких знаков подчёркивания. Например:

Было: vertex, vector4, vector3::mag, matrix::rotation_around_axis.
Стало: Vertex, Vector4, Vector3::Mag, Matrix::RotationAroundAxis.

math_v0.2.lib

Так как теперь имена классов пишутся с большой буквы, то нам придётся увеличить версию библиотеки math. math_v0.2.lib нельзя использовать в старых проектах, а версию math_v0.1.lib - в новых (с этого урока).

Библиотеку math_v0.2 можно скачать из раздела Листинги и программы. Скачать можно два варианта: весь проект math_v0.2. Тогда вам придётся самим компилировать библиотеку. Для загрузки проекта в IDE нужно запустить файл с расширением .vcproj. Второй вариант - уже скомпилированная библиотека. В этом архиве две папки с заголовочными и библиотечными файлами. Библиотека собрана в релизной конфигурации. Все настройки выставлены по умолчанию, единственное, я убрал флаг /GL (Whole program optimization), чтобы компоновщик не ругался на отсутствие /ltcg при сборке отладочной версии проекта.

Когда мы покдлючаем библиотеку math_v0.1.lib (и теперь math_v0.2.lib) к проекту, то при сборке компоновщик выдаёт предупреждение:

1>LINK : warning LNK4098: defaultlib 'MSVCRT' conflicts with use of other libs; use /NODEFAULTLIB:library

Т.е. библиотека MSVCRT конфликтует с пользовательской (нашей) библиотекой. Дальше указан способ решения проблемы: использовать флаг /NODEFAULTLIB, который игнорирует библиотеку. Мы так и поступим. Для этого в свойствах проекта в пункт Linker -> Input -> Ignore Specific Library (Компоновщик -> Ввод -> Игнорировать определённую библиотеку) нужно добавить имя файла:
Фото

При этом в параметры командной строки компоновщика добавится ключ: /NODEFAULTLIB:"MSVCRT".

На этом можно было бы закончить обсуждение библиотек, но давайте зададимся вопросом: а что находится в MSVCRT и почему компоновщик выдаёт предупреждение? В MSVCRT содержатся библиотеки времени исполнения программы. Игнорирование этого файла может повлечь в будущем серьёзные последствия (при определённых условиях).

Но почему же всё-таки эта библиотека конфликтует с нашей math? Проблема вот в чём: в файле vector3_v0.1.cpp включается заголовочный файл math.h (он там нужен для вызова функций модуля числа и корня), но файл math.h включается и в программу. Решением данной проблемы будет перенос включения файла math.h из файла vector3_v0.1.cpp в файл vector3_v0.1.h. Но и в этом случае у нас в будущем возникнет множество проблемных ситуаций. Как правильно разрешать подобные конфликты мы ещё обсудим. Пока же мы просто отключим библиотеку MSVCRT.

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

Класс Matrix

Когда мы начали изучать DirectX, мы использовали структуру D3DMATRIX. Затем мы написали ей замену - структуру matrix. Для программы камера нам понадобится возможность вращения объектов вокруг произвольных осей. Поэтому я написал отдельный класс Matrix, который поместил в math_v0.2.lib.

В классе Matrix - 16 переменных-членов. Идентификаторы этих переменных совпадают с соответствующими идентификаторами D3DMATRIX.

В Matrix всего два метода: конструктор без аргументов и RotationAroundAxis (вращение вокруг оси).

При создании объекта Matrix создаётся единичная матрица:

код на языке c++
Matrix::Matrix () : _11(1), _12(0), _13(0), _14(0),
                    _21(0), _22(1), _23(0), _24(0),
                    _31(0), _32(0), _33(1), _34(0),
                    _41(0), _42(0), _43(0), _44(1)
{}

Теперь не нужно инициализировать все элементы. Вот так вот теперь будет выглядеть инициализация проекционной матрицы:

код на языке c++
Matrix matProj;
matProj._33 = 100/99;
matProj._43 = -100/99;
matProj._34 = 1;
matProj._44 = 0;

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

Метод RotationAroundAxis создаёт матрицу вращения вокруг произвольной оси. Формула вращения вокруг произвольной оси рассматривается в последнем уроке по преобразованиям (смотрите ссылку в начале урока).

Методу передаётся трёхмерный вектор, вокруг которого будет производиться вращение, и угол:

код на языке c++
void Matrix::RotationAroundAxis(Vector3& v, float angle)
{
  _11 = v.x * v.x * (1 - cos(angle)) + cos(angle);
  _12 = v.x * v.y * (1 - cos(angle)) + v.z * sin(angle);
  _13 = v.x * v.z * (1 - cos(angle)) - v.y * sin(angle);

  _21 = v.x * v.y * (1 - cos(angle)) - v.z * sin(angle);
  _22 = v.y * v.y * (1 - cos(angle)) + cos(angle);
  _23 = v.y * v.z * (1 - cos(angle)) + v.x * sin(angle);

  _31 = v.x * v.z * (1 - cos(angle)) + v.y * sin(angle);
  _32 = v.y * v.z * (1 - cos(angle)) - v.x * sin(angle);
  _33 = v.z * v.z * (1 - cos(angle)) + cos(angle);
}

Данную формулу можно немного оптимизировать, если вычислить косинус и синус угла поворота заранее:

код на языке c++
float c = cos(angle);
float s = sin(angle);

Но на даннй момент это некритично, и я не стал этого делать, чтобы формула осталась понятной. :)

Указатели на функции

В программе камера мы воспользуемся такой штукой - указателями на функции.

Допустим у нас есть две функции:

код на языке c++
void MoveCube (int dx, int dy);
void MoveCam (int dx, ind dy);

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

код на языке c++
MoveCube(5,5);
MoveCube(3,3);
MoveCam(1,1);

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

код на языке c++
void (*MoveObject)(int dx, int dy);
void MoveCube (int dx, int dy);
void MoveCam (int dx, ind dy);

Аргументы и возвращаемое значение указателя на функцию совпадают с функциями MoveCam и MoveCube. У MoveObject нет определения, только прототип. Теперь, код перемещения:

код на языке c++
int input;
if ( input = 1)
  MoveObject = MoveCube;
if ( input = 2)
  MoveObject = MoveCam;

MoveObject(dx,dy);

Переменная input хранит выбор пользователя. 1 - пользователь выбрал куб, 2 - пользователь выбрал камеру. То есть в данном случае будет вызвана функция MoveCam или MoveCube в зависимости от значения переменной input.

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

код на языке c++
MoveObject = MoveCube;
MoveObject(5,5); // вызывается функция MoveCube
MoveOjbect(3,3); // вызывается функция MoveCube
MoveObject = MoveCam;
MoveObejct(1,1); // Вызывается функция MoveCam

Ещё раз о том, как нужно использовать указатели на функцию:

1. Аргументы и возвращаемое значения указателя на функцию должны совпадать с аргументами и возвращаемым значением функции, на которую будет указывать указатель.
2. О том что это указатель на функцию, говорит следующий синтаксис:
тип (*Идентификатор1)(список аргументов);
То есть имя функции заключено в круглые скобки и перед ним стоит звёздочка.
3. Присвоить указателю на функцию адрес другой функции:
идентификатор1 = идентификатор2; // идентификатор2 - имя обычной функции 4. Указатель на функцию вызывается как обычная функция, при этом происходит вызов той функции, на которую указывает указатель.

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

Файл classes.h

Код программы камера я разделил и поместил в два файла. Все классы хранятся в файле classes.h, в файле camera.cpp хранится код функции WinMain и нескольких дополнительных. Начнём рассматривать содержимое classes.h.

В файле содержится определение Vertex. Класс вершин не изменился с прошлого урока (за исключением первой буквы идентификатора).

Сюда же попала и функция multiplication. Но она была перегружена для трёхмерного вектора. Функция принимает трёхмерный (или четырёхмерный вектор) и матрицу Matrix размером 4x4. Внутри функции происходит перемножение двух метриц: матрицы строки (Vector4 или Vector3) и матрицы размером 4x4. Обратите внимание, что хотя в математике и нельзя перемножать матрицы 1x3 (Vector3) и 4x4 (Vector4), в функции multiplication это всё-таки происходит. Почему мы это делаем? В нашем случае мы используем перемножение multiplication(Vector3,Matrix) только в тех случаях, когда требуются линейные преобразования - прежде всего вращение.

Кроме того: сейчас функция multiplication как бы повисла в воздухе - она не принадлежит ни к одному классу, но при этом исползьуется с классами Matrix, Vector3, Vector4. Неплохо было бы поместить данную в функцию в какой-нибудь из перечисленных классов. С математической точки зрения функция должна принадлежать классу матриц, так как в теле функции происходит перемножение двух матриц. Соответствующий код будет выглядеть примерно так:

код на языке c++
Vector3 v;
Matrix m;
m.VectorMultiplication(v);
m*v; //  в случае перегруженного operator*
Если перемножение матриц поместить  в класс Vector,  то код будет выглядеть вот так:
v.MatrixMultiplication(m);
v*m;

Оба варианта как-то не очень. Ещё одно решение - оставить функцию multiplication (нужно только поменять имя) независимой и поместить в один из файлов с исходным кодом: vector.ccp или matrix.cpp. Код будет выглядеть примерно так:

Vector3MatrixMultiplication(v,m);
Какой из этих вариантов наилучший? К этом вопросу мы ещё вернёмся через несколько уроков. Пока что будем использовать функцию multiplication. Вот её код:

код на языке c++
void multiplication (Vector4& v, Matrix& m)
{
  v.x = v.x * m._11 + v.y * m._21 + v.z * m._31 + v.w * m._41;
  v.y = v.x * m._12 + v.y * m._22 + v.z * m._32 + v.w * m._42;
  v.z = v.x * m._13 + v.y * m._23 + v.z * m._33 + v.w * m._43;
  v.w = v.x * m._14 + v.y * m._24 + v.z * m._34 + v.w * m._44;
}

void multiplication (Vector3& v, Matrix& m)
{
  v.x = v.x * m._11 + v.y * m._21 + v.z * m._31;
  v.y = v.x * m._12 + v.y * m._22 + v.z * m._32;
  v.z = v.x * m._13 + v.y * m._23 + v.z * m._33;
}

Далее в файле classes.h расположена функция transformations. В данную функцию я поместил код, в котором происходит полное преобразование одной вершины.

код на языке c++
void transformations (Vertex& vertex, Vector4& vector, Matrix& matWorld,
                      Matrix& matCam, Matrix& matProj)
{
  Vector4 tempv = vector;
  multiplication(tempv,matWorld);
  multiplication(tempv,matCam);
  multiplication(tempv,matProj);
  tempv.x /= tempv.w;
  tempv.y /= tempv.w;
  tempv.z /= tempv.w;
  tempv.w /= tempv.w;
  vertex.x = tempv.x;
  vertex.y = tempv.y;
  vertex.z = tempv.z;
}

Самую интересную часть файла classes.h мы рассмотрим в следующей части урока.