Загрузка...
 
Печать

Камера вида от первого лица (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), где космический корабль может также перемещаться вверх, вниз, поворачиваться по любой из осей и двигаться в принципе в любом направлении.
Все эти сценарии мы рассмотрим в данной статье при создании нашей камеры.

Постановка задачи


Image
Рис.1 Оси вращения FP-камеры

Image
Рис.2 Питч камеры (наклон вверх-вниз) происходит по правой оси (right axis)

Image
Рис.3 Качение камеры происходит по оси взгляда (look at axis)

Image
Рис.4 Поворот камеры вокруг оси верха (up axis)


Наверное лучший способ представить себе камеру игрового персонажа от первого лица (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).
Теперь допустим, персонаж-камера отклонилась (pitch) вверх на 45 град. и вместе с тем повернулась (yaw) вправо на 45 град. При повороте по любой из осей мы также изменяем положение двух других осей. В нашем случае , в свете того, что правый вектор оказался незадействованным, остальные два вектора вращались относительно него. К примеру, если опустить голову вниз, то при этом изменится направление вектора взгляда (указывая на новые объекты внизу). Вместе с ним изменит направление вектор верха (up vector). Таким образом делаем вывод: Вращение предполагает трансформацию двух перпендикулярных векторов относительно третьего на определённый угол.

Двигаем камеру

Для перемещения персонажа-камеры вперёд (forward) тот перемещается в направлении своего вектора взгляда на определённое расстояние (являющееся относительной величиной, а не абсолютной). Другими словами он двигается в направлении своего взгляда с определённой скоростью.
Предположим, персонаж камера стоит в точке (0, 0, 0) и смотрит в точку (5, 0, 5). Для расчёта новой позиции мы выполним следующие шаги:
  1. Вычтем из координат вектора взгляда (5, 0, 5) координаты начальной точки (0, 0, 0): (5, 0, 5)-(0, 0, 0)
  2. Нормализуем полученный результирующий вектор.
  3. Умножим этот нормализованный вектор на скорость.
  4. Прибавим (add) полученный вектор к текущей позиции.
Другими словами, мы просто масштабируем (scale) вектор направления на определённую величину и прибавляем результат к начальной позиции (камеры).
Данный алгоритм также работает с любыми другими векторами. Например для стрейфа вправо мы масштабируем правый вектор и прибавляем полученное значение к координатам начальной точки. Для движения влево или назад масштабированный вектор вычитается из начальной позиции камеры.

Создаём камеру (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);
  • щаг в сторону (= стрейф) влево и вправо.
После этого вектору позиции камеры присваиваются новые координаты. Напомним, что движение вперёд совсем не означает перемещение по оси Z. Камера может быть повёрнута под любым углом и, как результат, её оси смещены.
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)

  • Самые важные дела происходят именно здесь.
  • Вызывается в каждом кадре.
Если позиция камеры и её вращение со времени последнего кадра не изменились, то матрица не перестраивается, а применяется та, что есть. Если при выполнении хотя бы одного преобразования флаг m_bChanged был установлен в true, то перестраиваем матрицу вида. Тем самым мы говорим Direct3D, где в готовящемся кадре стоит персонаж-камера, и как он ориентирован в 3D-пространстве. И происходит это путём указания соответствующих векторов в функциях, предшествующих вызову метода Update.
В самом простом случае для построения матрицы вида мы бы использовали функцию 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 функциональных блоков:
  1. Определяем, был ли где-нибудь флаг m_bChanged отмечен в true.
  2. Если нет, то сразу передаём в функцию SetTransform матрицу вида с предыдущего кадра без изменений. Если да, то перестраиваем матрицу вида на основе изменившихся параметров и также передаём её в функцию SetTransform.
  3. Процесс перестроения матрицы вида начинается с вызова функции D3DXMatrixRotationAxis. Это добавляет к исходным матрицам угол поворота (если есть) на указанное число радиан. Данная функция вызывается по одному разу для каждой из осей: Up, Right и LookAt. Значение угла поворота может быть 0 или любое другое, если тот был изменён с момента предыдущего кадра.
  4. После этого все три полученные матрицы трансформаций объединяются в одну матрицу MatTotal путём перемножения.
  5. Далее оси ориентации (верха, право и взгляда) трансформируются с матрицей MatTotal путём вызова функции D3DXVec3TransformCoord. Другими словами, происходит их вращение (относительно точки камеры) согласно данным комбинированной матрицы MatTotal. Третий вектор LookAt является перпендикуляром к плоскости, образованной двумя другими векторами (т.н. cross product; Подробнее по данной теме читай здесь: Базовые понятия 3D-графики). И потому его проще всего вычислить путём вызова функции D3DXVec3Cross.
  6. ОК, оси повёрнуты как надо. Теперь строим матрицу вида, которую затем передадим объекту устройства Direct3D (функция SetTransform). Перед построением матрицы вида ещё раз проверяем, что все три оси перпендикулярны друг другу. Проверяется значение с плавающей точкой (floating point), т.к. полученный дробный результат не следует округлять. Если не перпендикулярны, то перестроим (уже вектор m_Up) вектор как перпендикуляр к плоскости, образованной векторами m_Right и m_LookAt.
  7. Нормализуем все 3 вектора перед построением матрицы вида.
  8. Начинаем процесс построения матрицы вида. Начинаем с вычисления нижней строки (bottom row), каждый элемент которой является отрицательным значением скалярного произведения каждого из трёх векторов и точки позиции камеры (т.н. dot product; Подробнее об этом читаем здесь: https://function-x.ru/vectors_cosinus.html(external link)). После этого заполняем матрицу вида координатами векторов Up, Right и LookAt.

Источники


1. Thorn A. DirectX 9 graphics: the definitive guide to Direct3D. - Wordware Publishing, 2005


Последние изменения страницы Понедельник 20 / Июнь, 2022 12:30:50 MSK

Последние комментарии wiki

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++