Создание класса Камеры (C#, TaoFramework)
Что прочитать перед этим материалом
В этом уроке вы научитесь создавать класс “камера”.
Для начала необходимо разобраться с командой библиотеки Glu - GluLookAt. В нашем случаи Glu.gluLookAt. Она принимает три набора аргументов, которые задают точку наблюдения, прицельную точку (точку, на которую направлена камера) и направление, которое следует считать верхним.

Пример:
Код:
Glu.gluLookAt(Position.x, Position.y, Position.z, // Позиция самой камеры
View.x, View.y, View.z, // Направление, куда мы смотрим
Up.x, Up.y, Up.z); // Верх камеры
Вроде бы все просто. Теперь создадим отдельный класс в нашем приложении - Camera. Сразу подключим пространства и создадим Vertex3D - структуру, где будут храниться координаты X, Y, Z векторов.
Код:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tao.FreeGlut;
using Tao.OpenGl;
using Tao.Platform.Windows;
namespace Engine
{
class Camera
{
private struct Vector3D
{
public float x, y, z;
};
}
}
Объявим вектора которые будем использовать:
Код:
private Vector3D mPos; // Вектор позиции камеры
private Vector3D mView; // Направление, куда смотрит камера
private Vector3D mUp; // Вектор верхнего направления
private Vector3D mStrafe; // Вектор для стрейфа (движения влево и вправо) камеры
Далее мы создадим метод, который будет нам возвращать перпендикулярный вектор от трех переданных векторов. Два любых вектора образуют плоскость, от которой мы и ищем перпендикуляр. Это все нам понадобится для того, чтобы мы смогли реализовать Стрейф.
Код:
private Vector3D Cross(Vector3D vV1, Vector3D vV2, Vector3D vVector2)
{
Vector3D vNormal;
Vector3D vVector1;
vVector1.x = vV1.x - vV2.x;
vVector1.y = vV1.y - vV2.y;
vVector1.z = vV1.z - vV2.z;
// Если у нас есть 2 вектора (вектор взгляда и вертикальный вектор),
// У нас есть плоскость, от которой мы можем вычислить угол в 90 градусов.
// Рассчет cross'a прост, но его сложно запомнить с первого раза.
// Значение X для вектора = (V1.y * V2.z) - (V1.z * V2.y)
vNormal.x = ((vVector1.y * vVector2.z) - (vVector1.z * vVector2.y));
// Значение Y = (V1.z * V2.x) - (V1.x * V2.z)
vNormal.y = ((vVector1.z * vVector2.x) - (vVector1.x * vVector2.z));
// Значение Z = (V1.x * V2.y) - (V1.y * V2.x)
vNormal.z = ((vVector1.x * vVector2.y) - (vVector1.y * vVector2.x));
// *ВАЖНО* Вы не можете менять этот порядок, иначе ничего не будет работать.
// Должно быть именно так, как здесь. Просто запомните, если вы ищите Х, вы не
// используете значение X двух векторов, и то же самое для Y и Z. Заметьте,
// вы рассчитываете значение из двух других осей и никогда из той же самой.
// Итак, зачем всё это? Нам нужно найти ось, вокруг которой вращаться. Вращение камеры
// влево и вправо простое - вертикальная ось всегда (0, 1, 0).
// Вращение камеры вверх и вниз отличается, так как оно происходит вне
// глобальных осей. Достаньте себе книгу по линейной алгебре, если у вас
// её ещё нет, она вам пригодится.
// вернем результат.
return vNormal;
}
Далее создадим два метода который будут работать над векторами - Magnitude и Normalize.
Magnitude - возвращает величину вектора.
Код:
private float Magnitude(Vector3D vNormal)
{
// Это даст нам величину нашей нормали,
// т.е. длину вектора. Мы используем эту информацию для нормализации
// вектора. Вот формула: magnitude = sqrt(V.x^2 + V.y^2 + V.z^2) где V - вектор.
return (float)Math.Sqrt((vNormal.x * vNormal.x) +
(vNormal.y * vNormal.y) +
(vNormal.z * vNormal.z));
}
Normalize - возвращает нормализированный вектор, длинна которого равна единице, а это делает все рассчеты проще.
Код:
private Vector3D Normalize(Vector3D vVector)
{
// Вы спросите, для чего эта ф-я? Мы должны убедиться, что наш вектор нормализирован.
// Вектор нормализирован - значит, его длинна равна 1. Например,
// вектор (2, 0, 0) после нормализации будет (1, 0, 0).
// Вычислим величину нормали
float magnitude = Magnitude(vVector);
// Теперь у нас есть величина, и мы можем разделить наш вектор на его величину.
// Это сделает длинну вектора равной единице, так с ним будет легче работать.
vVector.x = vVector.x / magnitude;
vVector.y = vVector.y / magnitude;
vVector.z = vVector.z / magnitude;
return vVector;
}
Итак, теперь начнем работать с глобальными методами, которые мы сможем вызвать. Для начала нам нужно установить позицию, взгляд и вертикальный вектор камеры, в этом нам поможет следующий метод:
Код:
public void Position_Camera(float pos_x, float pos_y, float pos_z,
float view_x, float view_y, float view_z,
float up_x, float up_y, float up_z)
{
mPos.x = pos_x; // Позиция камеры
mPos.y = pos_y; //
mPos.z = pos_z; //
mView.x = view_x; // Куда смотрит, т.е. взгляд
mView.y = view_y; //
mView.z = view_z; //
mUp.x = up_x; // Вертикальный вектор камеры
mUp.y = up_y; //
mUp.z = up_z; //
}
Теперь дадим возможность крутить камеру вокруг своей оси, как при виде от первого лица. Передавая в метод переменную speed, будем управлять силой поворота, а также направлением. Если положительная переменная, то вправо, а отрицательная, как вы уже догадались, налево.
Код:
public void Rotate_View(float speed)
{
Vector3D vVector; // Полчим вектор взгляда
vVector.x = mView.x - mPos.x;
vVector.y = mView.y - mPos.y;
vVector.z = mView.z - mPos.z;
mView.z = (float)(mPos.z + Math.Sin(speed) * vVector.x + Math.Cos(speed) * vVector.z);
mView.x = (float)(mPos.x + Math.Cos(speed) * vVector.x - Math.Sin(speed) * vVector.z);
}
Перейдем к созданию вида от третьего лица. Чтобы реализовать вращение камеры, мы будем использовать axis-angle вращение.
Помните, что формулы для расчета вращения не очень просты, но занимают немного кода. Axis-angle вращение позволяет нам вращать точку в пространстве вокруг нужной оси. Это значит, что мы можем взять нашу точку взгляда (m_vView) и вращать вокруг нашей позиции. Чтобы лучше понять следующие рассчеты, советую вам посмотреть детальное описание: http://astronomy.swin.edu.au/~pbourke/geometry/rotate/
Также это функция реализована, чтобы в переменную angle вы передавали угол в градусах.
Код:
public void Rotate_Position(float angle, float x, float y, float z)
{
mPos.x = mPos.x - mView.x;
mPos.y = mPos.y - mView.y;
mPos.z = mPos.z - mView.z;
Vector3D vVector = mPos;
Vector3D AVector;
float SinA = (float)Math.Sin(Math.PI * angle / 180.0);
float CosA = (float)Math.Cos(Math.PI * angle / 180.0);
// Найдем новую позицию X для вращаемой точки
AVector.x = (CosA + (1 - CosA) * x * x) * vVector.x;
AVector.x += ((1 - CosA) * x * y - z * SinA) * vVector.y;
AVector.x += ((1 - CosA) * x * z + y * SinA) * vVector.z;
// Найдем позицию Y
AVector.y = ((1 - CosA) * x * y + z * SinA) * vVector.x;
AVector.y += (CosA + (1 - CosA) * y * y) * vVector.y;
AVector.y += ((1 - CosA) * y * z - x * SinA) * vVector.z;
// И позицию Z
AVector.z = ((1 - CosA) * x * z - y * SinA) * vVector.x;
AVector.z += ((1 - CosA) * y * z + x * SinA) * vVector.y;
AVector.z += (CosA + (1 - CosA) * z * z) * vVector.z;
mPos.x = mView.x + AVector.x;
mPos.y = mView.y + AVector.y;
mPos.z = mView.z + AVector.z;
}
Займемся перемещением камеры. В первую очередь, как вы поняли, нам нужна возможность перемещать камеру вперед и назад в зависимости от того, куда смотрит камера. Следующий метод позволит нам это делать.
Код:
public void Move_Camera(float speed) // Задаем скорость
{
Vector3D vVector; // Получаем вектор взгляда
vVector.x = mView.x - mPos.x;
vVector.y = mView.y - mPos.y;
vVector.z = mView.z - mPos.z;
vVector.y = 0.0f; // Это запрещает камере подниматься вверх
vVector = Normalize(vVector);
mPos.x += vVector.x * speed;
mPos.z += vVector.z * speed;
mView.x += vVector.x * speed;
mView.z += vVector.z * speed;
}
Сразу напишем Стрейф:
Код:
public void Strafe(float speed)
{
// добавим вектор стрейфа к позиции
mPos.x += mStrafe.x * speed;
mPos.z += mStrafe.z * speed;
// Добавим теперь к взгляду
mView.x += mStrafe.x * speed;
mView.z += mStrafe.z * speed;
}
Но для стрейфа нужно вычислять перпендикуляр каждый момент времени. Для этого напишем следующий метод:
Код:
public void update()
{
Vector3D vCross = Cross(mView, mPos, mUp);
// Нормализуем вектор стрейфа
mStrafe = Normalize(vCross);
}
Теперь добавим возможность камеры смотреть вверх и вниз.
Код:
public void upDown(float speed)
{
mPos.y += speed;
}
Нужен метод, который будет обновлять взгляд и позицию камеры:
Код:
public void Look()
{
Glu.gluLookAt(mPos.x, mPos.y, mPos.z, // Ранее упомянутая команда
mView.x, mView.y, mView.z,
mUp.x, mUp.y, mUp.z);
}
Итак, на всякий случай допишем методы, с помощью которых можно получать позиции в пространстве как и камеры, так и взгляда:
Код:
public double getPosX() // Возвращает позицию камеры по Х
{
return mPos.x;
}
public double getPosY() // Возвращает позицию камеры по Y
{
return mPos.y;
}
public double getPosZ() // Возвращает позицию камеры по Z
{
return mPos.z;
}
public double getViewX() // Возвращает позицию взгляда по Х
{
return mView.x;
}
public double getViewY() // Возвращает позицию взгляда по Y
{
return mView.y;
}
public double getViewZ() // Возвращает позицию взгляда по Z
{
return mView.z;
}
Ну вот и все. Класс написан и может быть использован.
Как же его применять?
Создадим новый проект. Объявим нужные нам библиотеки и пространства имен, добавим SimpleOpenGlControl и переименуем его в AnT. Также добавим Timer. Сразу объявим новый наш класс:
Код:
Camera cam = new Camera();
Напишем метод инициализации OpenGl:
Код:
private void InitGL()
{
Glut.glutInit();
Glut.glutInitDisplayMode(Glut.GLUT_RGB | Glut.GLUT_DOUBLE | Glut.GLUT_DEPTH);
Gl.glClearColor(255, 255, 255, 1);
Gl.glViewport(0, 0, AnT.Width, AnT.Height);
Gl.glMatrixMode(Gl.GL_PROJECTION);
Gl.glLoadIdentity();
Glu.gluPerspective(45, (float)AnT.Width / (float)AnT.Height, 0.1, 200);
Gl.glMatrixMode(Gl.GL_MODELVIEW);
Gl.glLoadIdentity();
Gl.glEnable(Gl.GL_DEPTH_TEST);
Gl.glEnable(Gl.GL_LIGHTING);
Gl.glEnable(Gl.GL_LIGHT0);
Gl.glBlendFunc(Gl.GL_SRC_ALPHA, Gl.GL_ONE_MINUS_SRC_ALPHA);
Gl.glHint(Gl.GL_LINE_SMOOTH_HINT, Gl.GL_NICEST);
Gl.glEnable(Gl.GL_BLEND);
Gl.glEnable(Gl.GL_LINE_SMOOTH);
Gl.glLineWidth(1.0f);
cam.Position_Camera(0, 6, -15, 0, 3, 0, 0, 1, 0); // Вот тут в инициализации
// укажем начальную позицию камеры, взгляда и вертикального вектора.
}
В загрузку формы кинем нашу инициализацию:
Код:
private void Form1_Load_1(object sender, EventArgs e)
{
InitGL();
}
Также напишем метод рисования:
Код:
private void Draw()
{
Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);
Gl.glLoadIdentity();
Gl.glColor3i(255, 0, 0);
cam.Look(); // Обновляем взгляд камеры
Gl.glPushMatrix();
DrawGrid(30, 1); // Нарисуем сетку
Gl.glPopMatrix();
Gl.glFlush();
AnT.Invalidate();
}
Метод рисования Сетки:
Код:
private void DrawGrid(int x, float quad_size)
{
// x - количество или длина сетки, quad_size - размер клетки
Gl.glPushMatrix(); // Рисуем оси координат, цвет объявлен в самом начале
Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_AMBIENT_AND_DIFFUSE, MatrixColorOX);
Gl.glTranslated((-x * 2) / 2, 0, 0);
Gl.glRotated(90, 0, 1, 0);
Glut.glutSolidCylinder(0.02, x * 2, 12, 12);
Gl.glPopMatrix();
Gl.glPushMatrix();
Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_AMBIENT_AND_DIFFUSE, MatrixColorOZ);
Gl.glTranslated(0, 0, (-x * 2) / 2);
Glut.glutSolidCylinder(0.02, x * 2, 12, 12);
Gl.glPopMatrix();
Gl.glPushMatrix();
Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_AMBIENT_AND_DIFFUSE, MatrixColorOY);
Gl.glTranslated(0, x / 2, 0);
Gl.glRotated(90, 1, 0, 0);
Glut.glutSolidCylinder(0.02, x, 12, 12);
Gl.glPopMatrix();
Gl.glBegin(Gl.GL_LINES);
Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_AMBIENT_AND_DIFFUSE, MatrixOXOYColor);
// Рисуем сетку 1х1 вдоль осей
for (float i = -x; i <= x; i += 1)
{
Gl.glBegin(Gl.GL_LINES);
// Ось Х
Gl.glVertex3f(-x * quad_size, 0, i * quad_size);
Gl.glVertex3f(x * quad_size, 0, i * quad_size);
// Ось Z
Gl.glVertex3f(i * quad_size, 0, -x * quad_size);
Gl.glVertex3f(i * quad_size, 0, x * quad_size);
Gl.glEnd();
}
}
Напишем метод, который будет считывать мышку:
Код:
private void AnT_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
mouseRotate = true; // Если нажата левая кнопка мыши
if (e.Button == MouseButtons.Middle)
mouseMove = true; // Если нажата средняя кнопка мыши
myMouseYcoord = e.X; // Передаем в нашу глобальную переменную позицию мыши по Х
myMouseXcoord = e.Y;
}
private void AnT_MouseUp(object sender, MouseEventArgs e)
{
mouseRotate = false;
mouseMove = false;
}
private void AnT_MouseMove(object sender, MouseEventArgs e)
{
myMouseXcoordVar = e.Y;
myMouseYcoordVar = e.X;
}
И конечно нам надо обработать мышку, вот и сам метод:
Код:
private void mouse_Events()
{
if (mouseRotate == true) // Если нажата левая кнопка мыши
{
AnT.Cursor = System.Windows.Forms.Cursors.SizeAll; //меняем указатель
cam.Rotate_Position((float)(myMouseYcoordVar - myMouseYcoord), 0, 1, 0); // крутим камеру, в моем случае вид у нее от третьего лица
rot_cam_X = rot_cam_X + (myMouseXcoordVar - myMouseXcoord);
if ((rot_cam_X > -40) && (rot_cam_X < 40))
cam.upDown(((float)(myMouseXcoordVar - myMouseXcoord)) / 10);
myMouseYcoord = myMouseYcoordVar;
myMouseXcoord = myMouseXcoordVar;
}
else
{
if (mouseMove == true)
{
AnT.Cursor = System.Windows.Forms.Cursors.SizeAll;
cam.Move_Camera((float)(myMouseXcoordVar - myMouseXcoord) / 50);
cam.Strafe(-((float)(myMouseYcoordVar - myMouseYcoord) / 50));
myMouseYcoord = myMouseYcoordVar;
myMouseXcoord = myMouseXcoordVar;
}
else
{
AnT.Cursor = System.Windows.Forms.Cursors.Default; // возвращаем курсор
};
};
}
И дописываем в таймер.
Код:
mouse_Events();
cam.update();
Draw();
Вот и все. На данном примере мы сделали возможность перемещаться по сцене, как в редакторе моделей 3D Studio Max. Этот метод можно использовать и в играх (стратегиях).