Организация камеры в 3D играх
В данной статье я постараюсь показать принципы организации камеры в играх.
Приведенный метод не является чем-то новым и доселе неизвестным, это лишь мои наработки в этой области, которые помогут быстро и удобно реализовать камеру в игре.
Итак, что такое камера в 3D игре? Это виртуальное "око" игрока, то, посредством чего он воспринимает игру визуально. В понятие "камера" входят: угол обзора и положение которое задается радиус вектором и 3 углами относительно осей координат.
Один из самых простых методов выглядит так:
procedure TCamera.SetRender;
begin
gluLookAt(e.X, e.Y, e.Z, c.X, c.Y, c.Z, Up.X, Up.Y, Up.Z);
end;
В процедуру gluLookAt передается всего 3 радиус-вектора:
e – точка в которую обращена камера
c – положение камеры в пространстве
Up – указывает направление "вверх" для камеры.
По-поводу первых двух надеюсь вопросов нет, но вот с вычислением третьего придется изрядно попотеть…
Однако, зачем что-то вычислять, если это можно доверить графическому API?
procedure TCamera.SetRender;
begin
glLoadIdentity;
glRotatef(Angle.Z, 0, 0, 1);
glRotatef(Angle.X, 1, 0, 0);
glRotatef(Angle.Y, 0, 1, 0);
glTranslatef(-Pos.X, -Pos.Y, -Pos.Z);
end;
где
Angle – вектор описывающий углы поворота относительно каждой из осей координат (в градусах);
Pos – положение камеры в пространстве, также задающееся радиус-вектором.
Данный метод бесспорно и прост и удобен, но такие операции как glRotate и glTranslate производят умножение видовой матрицы на другую матрицу. Это конечно же не критично для современных компьютеров, но все же вполне оптимизируемо. Чем мы и займемся…
Для того чтобы что-либо оптимизировать мы должны понять принцип работы всех трех операций.
Как известно, перед выводом геометрии на дисплей над ней производится несколько операций, а именно – умножение на матрицу вида и матрицу проекции.
Нам же достаточно работы с матрицей вида (MODELVIEW). Сама же матрица вида представляется 16 вещественными числами, т.е. матрица имеет размерность 4х4.
Итак, разберем все операции из предыдущего примера по отдельности:
glLoadIdentity – преобразует текущую (видовую, проекции, текстуры) матрицу в единичную
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
glRotatef – домножает текущую матрицу вида на матрицу поворота относительно одной из осей координат.
Имея 3 оси координат, соответственно можно вычислить всего 3 матрицы поворота:
Относительно оси OX:
1 0 0 0
0 c s 0
0 –s c 0
0 0 0 1
Относительно оси OY:
c 0 -s 0
0 1 0 0
s 0 c 0
0 0 0 1
Относительно оси OZ:
c s 0 0
-s c 0 0
0 0 1 0
0 0 0 1
где s и c – соответственно синусы и косинусы угла поворота.
glTranslatef – производит домножение матрицы вида на матрицу сдвига:
1 0 0 0
0 1 0 0
0 0 1 0
x y z 1
где x, y, z – приращение к соответствующим координатам векторов в новой системе координат.
Итак, с сутью операций разобрались, теперь можно приступить к оптимизации, которая будет заключаться в ручном вычислении матрицы вида!
Для этого нам понадобятся 3 угла и позиция камеры.
Нам необходимо перемножить 3 матрицы поворота, и порядок их перемножения которых имеет большое значение.
В итоге, перемножение матриц [Z]*[X]*[Y] будет выглядеть так:
[ E F 0 ] [ 1 0 0 ] [ C 0 -D ] [ CE+BDF AF BCF-ED ]
[-F E 0 ] X [ 0 A B ] X [ 0 1 0 ] = [ BDE-CF AE DF+BCE ]
[ 0 0 1 ] [ 0 -B A ] [ D 0 C ] [ AD -B AC ]
где
A = cos(Angle.X);
B = sin(Angle.X);
C = sin(Angle.Y);
D = cos(Angle.Y);
E = cos(Angle.Z);
F = sin(Angle.Z);
Заметьте, что C и D определены "не верно", т.к. мы попутно приводим матрицу к некоему базису. Это связано с направлением оси Z в OpenGL.
Теперь необходимо рассчитать матрицу вида, которая выгладит так:
x.x x.y x.z -dot(x, Pos)
y.x y.y y.z -dot(y, Pos)
z.x z.y z.z -dot(z, Pos)
0 0 0 1
где x, y, z – вектора построенные на соответствующих компонентах матрицы:
x = (CE+BDF, AF, BCF-ED)
y = (BDE-CF, AE, DF+BCE)
z = ( AD, -B, AC)
Pos – положение камеры в пространстве.
Операция dot осуществляет скалярное произведение векторов.
И сам код осуществляющий расчет:
procedure TCamera.SetRender;
var
A, B, C, D, E, F : single;
cx, cy, cz : TVector;
begin
with Angle do
begin
A := cos(X);
B := sin(X);
C := sin(Y);
D := cos(Y);
E := cos(Z);
F := sin(Z);
end;
cx := Vector(C*E+B*D*F, A*F, B*C*F-E*D);
cy := Vector(B*D*E-C*F, A*E, D*F+B*C*E);
cz := Vector(A*D, -B, A*C);
// заполнение матрицы
m[0]:=cx.X; m[4]:=cx.Y; m[8] :=cx.Z; m[12]:=-V_Dot(cx, Pos);
m[1]:=cy.X; m[5]:=cy.Y; m[9] :=cy.Z; m[13]:=-V_Dot(cy, Pos);
m[2]:=cz.X; m[6]:=cz.Y; m[10]:=cz.Z; m[14]:=-V_Dot(cz, Pos);
m[3]:=0; m[7]:=0; m[11]:=0; m[15]:=1;
// установка матрицы проекции
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluPerspective(FOV, Width/Height, 0.1, 100);
glMatrixMode(GL_MODELVIEW);
// установка матрицы вида
glLoadMatrixf(@m);
end;
Здесь матрица описывается в виде одномерного массива из 16 элементов типа single.
m: array [0..15] of single;
Углы (Angle) задаются в радианах.
FOV – угол обзора камеры, который рекомендуется ставить равным 90;
Width и Height - ширина и высота поля вывода соответственно;
Vector – функция создания переменной типа TVector по трем значениям (x, y, z);
V_Dot – скалярное умножение векторов (x1*x2 + y1*y2 + z1*z2).
Сама процедура выглядит устрашающе по сравнению со вторым методом, но работает значительно шустрее. Вызов ее рекомендуется производить перед началом рендеринга карты и объектов на ней, чтобы изменения матрицы вида отразились на их выводе.