Камера вида от первого лица (First Person Camera)
C++, Win32, DirectX 9Содержание
Intro
Специальный объект камера (viewer) контролирует матрица вида (view matrix).1В DirectX 9 для расчёта положения и ориентации камеры в 3D-пространстве применяют специальную функцию D3DXMatrixLookAtLH (для леворучной картезианской системы координат).
В этой статье мы создадим полнофункциональную 3D-камеру, заточенную для применение в играх жанра 3D FPS (Quake, Half-Life и др.). К концу статьи объект камера будет инкапсулирован в отдельный класс, содержащий следующие фичи:
- Возможность крутить камеру вверх, вниз, влево и вправо.
- Возможность перемещать камеру вперёд, назад + шагать (= strafe, стрейфиться) влево и вправо.
- Возможность определять, какие из объектов сцены видны камерой в данный момент времени (оптимизация методом усечённой пирамиды вида).
Проблема
Представь, что создаёшь игру в жанре First Person Shooter (FPS). То есть игру, где игрок видит сцену глазами персонажа, которым управляет. Неопытный игрокодер отследит положение + ориентацию вектора взгляда камеры и будет "скармливать" эти данные функции D3DXMatrixLookAtLH практически в каждом кадре, где она двигается. Но это плохая идея. Допустим, персонаж-камера по команде игрока перемещается на 3 единицы (unit) вперёд. В этом случае мы просто увеличим Z-компонент положения камеры на 3. При перемещении (стрейфе) игрока влево на 5 единиц, мы уменьшим X-компоненту персонажа-камеры на 5. Сложности возникнут, когда персонаж одновременно переместится вперёд, в сторону, повернётся по оси Y на -PI/2 радиан (т.е. влево), а затем переместится вперёд ещё на 5 единиц. После поворота камера-персонаж уже не ориентирована по оси Z. Задача ещё более усложняется в случае игры в жанре космосим (space simulator), где космический корабль может также перемещаться вверх, вниз, поворачиваться по любой из осей и двигаться в принципе в любом направлении.Все эти сценарии мы рассмотрим в данной статье при создании нашей камеры.
Постановка задачи
Наверное лучший способ представить себе камеру игрового персонажа от первого лица (First Person Camera) - это посмотреть на голову человека. Она способна (ограничено) вращаться по трём различным осям, каждая из которых перпендикулярна другой. Воображаемая стрелка указывает направление "взгляда" камеры (т.н. "вектор взгляда", look at vector). Другая - направлена вправо (т.е. перпендикулярна "правому уху" персонажа) и называется "правый вектор" (right vector). Третья смотрит строго вверх и называется "вектор верха" (up vector). Точка пересечения этих векторов представляет собой текущую позицию камеры в 3D-пространстве (См. Рис.1).
Посмотри вокруг
Три оси, положительные лучи которых сходятся в одной точке, позволяют вращать камеру по ним. Рассмотрим вращение более подробно. Мы можем вращать камеру (как и голову) по оси взгляда (look at Pitch axis), оси верха (up axis), правой оси (right axis) и даже по всем трём одновременно. Вращение камеры по оси верха и правой оси влияет на направление вектора взгляда.Pitch (отклонение)
При повороте персонажа-камеры на 45 град. вниз или вверх произойдёт вращение по правой оси (right axis), также называемое питчем (pitch; См. Рис.2).Roll (качение)
Исходная позиция - персонаж-камера смотрит прямо перед собой. Затем она наклоняется (tilt) влево или вправо. И снова возвращается в начальное положение. Такое вращение вокруг оси взгляда (look at axis) называют качением (roll; См. Рис.3).Yaw (поворот)
- Осуществляется в горизонтальной плоскости по оси верха (up axis; См. Рис.4).
- В том же значении термин применяется в авиации.
Комбинирование вращений (Combining Rotations)
С точки зрения программного объекта "камера" во время её вращения происходит ряд преобразований. Допустим, персонаж-камера стоит и смотрит прямо перед собой. При этом векторы её осей нормализуются (т.к. их размер в этом случае не важен). В этой исходной позиции:- точка позиции камеры (0, 0, 0);
- вектор взгляда (look at vector) (0, 0, 1);
- правый вектор (right vector) (1, 0, 0);
- вектор верха (up vector) (0, 1, 0).
Двигаем камеру
Для перемещения персонажа-камеры вперёд (forward) тот перемещается в направлении своего вектора взгляда на определённое расстояние (являющееся относительной величиной, а не абсолютной). Другими словами он двигается в направлении своего взгляда с определённой скоростью.Предположим, персонаж камера стоит в точке (0, 0, 0) и смотрит в точку (5, 0, 5). Для расчёта новой позиции мы выполним следующие шаги:
- Вычтем из координат вектора взгляда (5, 0, 5) координаты начальной точки (0, 0, 0): (5, 0, 5)-(0, 0, 0)
- Нормализуем полученный результирующий вектор.
- Умножим этот нормализованный вектор на скорость.
- Прибавим (add) полученный вектор к текущей позиции.
Данный алгоритм также работает с любыми другими векторами. Например для стрейфа вправо мы масштабируем правый вектор и прибавляем полученное значение к координатам начальной точки. Для движения влево или назад масштабированный вектор вычитается из начальной позиции камеры.
Создаём камеру (DirectX 9)
Пишем объявление класса камеры:class CXCamera { protected: // Переменные для хранения позиции и трёх векторов. D3DXVECTOR3 m_Position; D3DXVECTOR3 m_LookAt; D3DXVECTOR3 m_Right; D3DXVECTOR3 m_Up; // Спецметка, отмечающая изменилось ли положение камеры после последнего обновления. // Подразумевается изменение её положения или вращение. Если да, то надо пересчитывать // матрицу. bool m_bChanged; // Переменные хранят углы вращения по каждой из осей. float m_RotateAroundUp; float m_RotateAroundRight; float m_RotateAroundLookAt; // Результирующая матрица трансформации вида D3DXMATRIX m_MatView; // Функция обновления итоговой матрицы вида в каждом кадре. VOID Update();
Инициализируем класс камеры
Первым делом убеждаемся, что членам класса присвоены начальные значения. Это выполняется в конструкторе класса:CXCamera::CXCamera() { m_Position = D3DXVECTOR3(0.0f, 0.0f, 0.0f); m_LookAt = D3DXVECTOR3(0.0f, 0.0f, 1.0f); m_Right = D3DXVECTOR3(1.0f, 0.0f, 0.0f); m_Up = D3DXVECTOR3(0.0f, 1.0f, 0.0f); m_RotateAroundUp = m_RotateAroundRight = m_RotateAroundLookAt = 0; D3DXMatrixIdentity(&m_MatView); }
Двигаем камеру
Здесь возможны следующие варианты перемещения:- движение вперёд (forward) и назад (backward);
- щаг в сторону (= стрейф) влево и вправо.
void CXCamera::SetPosition(D3DXVECTOR3 *Pos) { m_Position = *Pos; m_bChanged = true; } void CXCamera::MoveForward(float Dist) { m_Position += Dist * m_LookAt; m_bChanged = true; } void CXCamera::MoveRight(float Dist) { m_Position += Dist * m_Right; m_bChanged = true; } void CXCamera::MoveUp (float Dist) { m_Position += Dist * m_Up; m_bChanged = true; } void CXCamera::MoveInDirection(float Dist, D3DXVECTOR3 *Dir) { m_Position += Dist * (*Dir); m_bChanged = true; }
Вращаем камеру
Угол вращения указываем в водном параметре в радианах. Вращение возможно по любой из трёх осей и даже по всем трём одновременно.void CXCamera::RotateUpDown(float Angle) { m_RotateAroundRight += Angle; m_bChanged = true; } void CXCamera::RotateLeftRight(float Angle) { m_RotateAroundUp += Angle; m_bChanged = true; } void CXCamera::Roll(float Angle) { m_RotateAroundLookAt += Angle; m_bChanged = true; } }
Метод Update. Строим матрицу вида (view matrix)
- Самые важные дела происходят именно здесь.
- Вызывается в каждом кадре.
В самом простом случае для построения матрицы вида мы бы использовали функцию D3DXMatrixLookAtLH. Но в нашем случае она не подходит, и мы будем строить матрицу вида вручную, с помощью функции Update. Вот её реализация:
bool CXCamera::Update() { if(m_bChanged) { // Матрицы, для хранения трансформации относительно осей. D3DXMATRIX MatTotal; D3DXMATRIX MatRotateAroundRight; D3DXMATRIX MatRotateAroundUp; D3DXMATRIX MatRotateAroundLookAt; // Получаем матрицу для каждого вида вращения. D3DXMatrixRotationAxis(&MatRotateAroundRight, &m_Right, m_RotateAroundRight); D3DXMatrixRotationAxis(&MatRotateAroundUp, &m_Up, m_RotateAroundUp); D3DXMatrixRotationAxis(&MatRotateAroundLookAt, &m_LookAt, m_RotateAroundLookAt); // Комбинируем (= перемножаем) все трансформации в одну общую матрицу. D3DXMatrixMultiply(&MatTotal, &MatRotateAroundUp, &MatRotateAroundRight); D3DXMatrixMultiply(&MatTotal, &MatRotateAroundLookAt, &MatTotal); // Трансформируем исходные векторы по полученным матрицам. D3DXVecTransformCoord(&m_Right, &m_Right, &MatTotal); D3DXVecTransformCoord(&m_Up, &m_Up, &MatTotal); // Вычисляем вектор LookAt, являющийся перпендикуляром к плоскости, // образованной двумя другими векторами (cross product). D3DXVec3Cross(&m_LookAt, &m_Right, &m_Up); // Проверяем, перпендикулярны ли векторы друг другу после всех преобразований. if(fabs(D3DXVec3Dot(&m_Up, &m_Right)) > 0.01) { // Если не перпендикулярны, то... D3DXVec3Cross(&m_Up, &m_LookAt, &m_Right); } // Нормализуем векторы. D3DXVec3Normalize(&m_Right, &m_Right); D3DXVec3Normalize(&m_Up, &m_Up); D3DXVec3Normalize(&m_LookAt, &m_LookAt); // Вычисляем нижнюю строку (bottom row) матрицы вида. float fView41, fView42, fView43; fView41 = -D3DXVec3Dot(&m_Right, &m_Position); fView42 = -D3DXVec3Dot(&m_Up, &m_Position); fView43 = -D3DXVec3Dot(&m_LookAt, &m_Position); // Заполняем матрицу вида. m_MatView = D3DXMATRIX(m_Right.x, m_Up.x, m_LookAt.x, 0.0f, m_Right.y, m_Up.y, m_LookAt.y, 0.0f, m_Right.z, m_Up.z, m_LookAt.z, 0.0f, fView41, fVi ew4 2, fView43, 1.0f); // Назначаем полученную матрицу вида в качестве действующей матрицы вида // текущего объекта устройства Direct3D. m_pDevice->SetTransform(D3DTS_VIEW, &m_MatView); // Сбрасываем временные переменные. m_RotateAroundRight = m_RotateAroundUp = m_RotateAroundLookAt = 0.0f; m_bChanged = false; } }
Весь код выше можно условно разделить на 8 функциональных блоков:
- Определяем, был ли где-нибудь флаг m_bChanged отмечен в true.
- Если нет, то сразу передаём в функцию SetTransform матрицу вида с предыдущего кадра без изменений. Если да, то перестраиваем матрицу вида на основе изменившихся параметров и также передаём её в функцию SetTransform.
- Процесс перестроения матрицы вида начинается с вызова функции D3DXMatrixRotationAxis. Это добавляет к исходным матрицам угол поворота (если есть) на указанное число радиан. Данная функция вызывается по одному разу для каждой из осей: Up, Right и LookAt. Значение угла поворота может быть 0 или любое другое, если тот был изменён с момента предыдущего кадра.
- После этого все три полученные матрицы трансформаций объединяются в одну матрицу MatTotal путём перемножения.
- Далее оси ориентации (верха, право и взгляда) трансформируются с матрицей MatTotal путём вызова функции D3DXVec3TransformCoord. Другими словами, происходит их вращение (относительно точки камеры) согласно данным комбинированной матрицы MatTotal. Третий вектор LookAt является перпендикуляром к плоскости, образованной двумя другими векторами (т.н. cross product; Подробнее по данной теме читай здесь: Базовые понятия 3D-графики). И потому его проще всего вычислить путём вызова функции D3DXVec3Cross.
- ОК, оси повёрнуты как надо. Теперь строим матрицу вида, которую затем передадим объекту устройства Direct3D (функция SetTransform). Перед построением матрицы вида ещё раз проверяем, что все три оси перпендикулярны друг другу. Проверяется значение с плавающей точкой (floating point), т.к. полученный дробный результат не следует округлять. Если не перпендикулярны, то перестроим (уже вектор m_Up) вектор как перпендикуляр к плоскости, образованной векторами m_Right и m_LookAt.
- Нормализуем все 3 вектора перед построением матрицы вида.
- Начинаем процесс построения матрицы вида. Начинаем с вычисления нижней строки (bottom row), каждый элемент которой является отрицательным значением скалярного произведения каждого из трёх векторов и точки позиции камеры (т.н. dot product; Подробнее об этом читаем здесь: https://function-x.ru/vectors_cosinus.html). После этого заполняем матрицу вида координатами векторов Up, Right и LookAt.
Источники
1. Thorn A. DirectX 9 graphics: the definitive guide to Direct3D. - Wordware Publishing, 2005
Последние изменения страницы Понедельник 20 / Июнь, 2022 12:30:50 MSK
Последние комментарии wiki