Камера. Движение. Видовая матрица.

 Сразу предупрежу читателя - все то, о чем говорится в этом обзоре, часто можно выполнить используя стандартные вспомогательные библиотеки, например GLU в OpenGL или библиотеки почившего нас Retained режима в Direct3D. Но ведь лишние знания никому не помешают ? :) Тогда вперед. Немного вспомним о самой как ее называют видовой матрице. В общем видовая матрица выполняет при умножении на нее всю основную работу по:
  • Изменению масштаба.
  • Вращению.
  • Переносу.
  • Перспективным преобразованиям.
 Теперь к сути проблемы. Для лучшего ее понимания я рекомендую скачать ПРИМЕР, который я писал для статей посвященных программированию с использованием OpenGL. Да это обыкновенный кубик... Нет - теперь прояви немного воображения и представь себе, что это некая трехмерная модель твоего космического крейсера или самолета. Верхняя грань кубика (зеленого цвета) соответственно верх корабля. Теперь я хочу, чтобы ты сменил "курс" нажав курсорную клавишу "стрелка назад" и поднял нос на 90 градусов так, чтобы верх нашего вымышленного корабля был направлен к нам. Отлично. Теперь мы как бы смотрим на него сверху. И последнее - поверни корабль на 45 градусов направо. Что ? Не выходит ? Нет нет - все верно - верни наш "крейсер" в исходное положение и убедись что стрелки влево и вправо все же работают, затем опять подними нос. Вместо того, чтобы поворачивать, наш "крейсер" крутит бочки. Проблема в том, что выполнив вращение вокруг оси X, мировые оси координат остались прежними - то есть сохранили свое направление, а вот локальные оси модели нет.
 Решение назревает само собой - мы должны сохранить локальные оси модели. При выполнении вращения по одной из осей - вращать на соответствующий угол две других локальных оси. Сразу же перейдем к ПРИМЕРУ этой статьи. Написан он с использованием OpenGL на основе все того шаблона. Локальные оси координат определены в примере тремя векторами - vlook,vup,vright. Предназначение всех трех понятно даже интуитивно.
  • vlook - определяет направление нашего взгяда. Однако, одного этого вектора конечно же недостаточно - например, мы можем смотреть в одну и ту же точку стоя на ногах и на голове(то есть этот вектор будет одинаковым и в первом и во втором случае). Этот вектор можно назвать локальной Z осью.
  • vup - локальная ось Y.
  • vright - локальная ось X.
Строго говоря - всегда достаточно иметь только два из них - локальную ось X можно получить выполнив вектороное произведение (Cross Product) первых двух - Z и Y.
 Продолжим. Для того, что бы повернуть соответствующую локальную ось на определенный угол, нужно подготовить матрицу вращения на данный угол, а затем перемножить полученную матрицу на вектор оси.
  • Получим матрицу вращения
    
    void ViewMatrixRotationAxis(float *vector,float angle)
    {
    	glLoadIdentity();	
    	glRotatef(angle,vector[0],vector[1],vector[2]);
    	return;
    }
    
    
    Теперь текущая видовая матрица имеет необходимый нам вид.
  • Изменим ось:
    
    void TransformAxis(float *vector)
    {	
    	float matrix[16],tempv[4];
    
    	glGetFloatv(GL_MODELVIEW_MATRIX,matrix); // получим текущую видовую матрицу
    
    	for (int j=0;j<4;j++)	
    	{
    		// выполним умножение...
    		tempv[j] =	matrix[j*4]*vector[0] + 
    					matrix[j*4+1]*vector[1] + 
    					matrix[j*4+2]*vector[2] + 
    					matrix[j*4+3]*vector[3];
    	}
    
    	memcpy(vector,tempv,sizeof(float)*4);
    
    	return;
    }
    
    
На заметку:

 С Direct3D начиная с версии DirectX SDK 7.0 идет вспомогательная библиотека 3DX. Библиотека включает в себя большое количество ПОЛЕЗНЫХ математических функций которыми стоит воспользоваться. Для использования функций библиотеки необходимо включить заголовочный файл d3dx.h, перед использованием библиотеки вызвать D3DXInitialize(), а по завершении работы D3DUninitialize(). проект необходимо линковать с библиотекой d3dx.lib
 В нашем случае тебя могут заинтересовать следующие функции этой библиотеки:
  • D3DXMatrixRotationAxis()
  • D3DXVec3TransformCoord()
  • D3DXVec3Normalize()
  • D3DXVec3Cross()
 Все идет достаточно неплохо, но это только на первый взгляд. Из-за погрешностей в операциях с плавающей точкой, уже после пары вращений локальные оси координат теряют свою перпендикулярность. Для того, что бы компенсировать это явление, необходимо выполнить регенирацию или как ее еще называют рекалибрацию векторов: (Внимание ! В первой версии этой статьи здесь была допущена ошибка - не нужно забывать, что операция векторного произведения векторов не коммутативна, а соответственно порядок следования аргументов для CrossProduct() ВАЖЕН !)

void RegenerateVectors(void)
{
	NormalizeVector(vlook);

	CrossProduct(vup,vlook,vright); // векторно перемножим VUP на VLOOK
	NormalizeVector(vright);

	CrossProduct(vlook,vright,vup); // векторно перемножим VLOOK на VRIGHT
	NormalizeVector(vup);
	
	return;	
}

 Идея калибрации видна из самого текста программы - необходимо выполнить серию нормализаций и векторных произведений векторов. Обрати внимание на то, что вектор vlook лишь нормализуется - но не регенерируется из других. Эту операцию необходимо выполнять первой - до формирования самой матрицы или операций с осями. Немного отвлекусь и скажу в кратце о движении - поскольку после регенерации все три вектора осей являются еденичными (нормализованными, то есть их значения не привышают еденицы), мы можем достаточно просто, отталкиваясь от текущего положения в пространстве и вектора направления вычислить смещение по этому вектору. Например для того, чтобы имитировать движение вперед (вдоль вектора vlook) нам необходимо каждую компоненту координаты текущего положения увеличить на произведение соответствующей компоненты вектора на текущую "скрость":

for (int j=0;j<3;j++) 
{
	cur_pos[j] += speed * vlook[j];
}

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

vright.x vup.x vlook.x 0
vright.y vup.y vlook.y 0
vright.z vup.z vlook.z 0
-DotProduct(Текущее положение,vright) -DotProduct(Текущее положение,vup) -DotProduct(Текущее положение,vlook) 1

где DotProduct - скалярное произведение векторов.

 А теперь это же в программе:


	glLoadIdentity();

	glGetFloatv(GL_MODELVIEW_MATRIX,mview);

	mview[0]  = vright[0];
	mview[1]  = vup[0];
	mview[2]  = vlook[0];

	mview[4]  = vright[1];
	mview[5]  = vup[1];
	mview[6]  = vlook[1];

	mview[8]  = vright[2];
	mview[9]  = vup[2];
	mview[10] = vlook[2];

	mview[12] = -DotProduct(vright,cur_pos);	
	mview[13] = -DotProduct(vup,cur_pos);
	mview[14] = -DotProduct(vlook,cur_pos);

	glLoadMatrixf(mview); // сделаем активной новоиспеченную матрицу...

 Важно то, что скалярное произведение векторов для членов 12,13,14 видовой матрицы должно быть отрицательным.

Эта статья является интеллектуальной собственностью автора (JM). Перепечатка или публикация отдельных ее частей или целиком разрешена с обязательным указанием источника.