Урок 6. Работа с клавиатурой и мышкой

Наконец-то мы добрались до 6 урока!
В этом уроке мы будем учиться использовать клавиатуру у мышку, чтобы двигать камеру как в игре шутере.

Интерфейс

Так как этот код будет использоваться везде в последующих уроках, давайте поместим код в отдельный файл common/controls.cpp, а сами функции объявим в файле common/controls.h поэтому в следующих уроках нам не придется писать этот код заново.

В этом уроке не будет таких прям уж больших изменений по сравнению с нашими предыдущими наработками. Самое большое отличие — мы будем вычислять МВП матрицу не один раз, при старте приложения, а каждый кадр. Для этого давайте сначала перенесем этот код в главный цикл:
do{
   // ...
   // Вычисляем МВП матрицу в зависимости от нажатий клавиш и двигания мышкой
   computeMatricesFromInputs();
   glm::mat4 ProjectionMatrix = getProjectionMatrix();
   glm::mat4 ViewMatrix = getViewMatrix();
   glm::mat4 ModelMatrix = glm::mat4(1.0);
   glm::mat4 MVP = ProjectionMatrix * ViewMatrix * ModelMatrix;
   // ...
}

В этом коде у нас есть 3 новых функции:
·         computeMatricesFromInputs() читаем состояние клавиатуры и мыши и вычисляем матрицы Проекции и Вида. В этой функции и будет происходить вся основная магия.
·         getProjectionMatrix() возвращает вычисленную матрицу Проекции.
·         getViewMatrix() возвращает вычисленную матрицу Вида.

Это, конечно, лишь один из способов создания подобной функциональности. Если вам он не нравится, переделайте как хотите.

Код выполняющий всю магию

Нам нужно несколько переменных:
// позиция
glm::vec3 position = glm::vec3( 0, 0, 5 );
// горизонтальный угол : по -Z
float horizontalAngle = 3.14f;
// вертикальный угол : 0, смотрим на горизонт
float verticalAngle = 0.0f;
// Угол обзора
float initialFoV = 45.0f;
float speed = 3.0f; // 3  в секунду
float mouseSpeed = 0.005f;

О FoV можно думать как об уровне увеличения. 80 — очень широкий угол, все уменьшено и сильно искажено. 60-45 — нормальный вид. 20 — подзорная труба.

Нам нужно будет пересчитывать позицию, горизонтальный угол и вертикальный угол и угол обзора в зависимости от действий пользователя.

Ориентация

Читать ввод с мышки очень просто:
// получаем координаты курсора
int xpos, ypos;
glfwGetMousePos(&xpos, &ypos);

но нам нужно не забывать о том, что необходимо передвигать курсор назад в центр экрана, иначе он вскоре выйдет за пределы окна, или упрется в край монитора и больше не будет двигаться.
// Двигаем мышь в центр экрана
glfwSetMousePos(1024/2, 768/2);

Заметьте, код предполагает, что окно у нас размера 1024*768, что, конечно, не всегда верно. Если хотите сделать красивее, можно получить размеры окна функцией glfwGetWindowSize.

Теперь вычислим необходимые углы:
horizontalAngle += mouseSpeed * deltaTime * float(1024/2 - xpos );
verticalAngle   += mouseSpeed * deltaTime * float( 768/2 - ypos );

Краткое описание того, что делает этот код:
  • 1024/2 — положение по Х. Чем дальше мы от центра, тем на больший угол нужно повернуться.
  • float(...) преобразовываем всем в дробные числа
  • mouseSpeed — коефициент, чтобы ускорить или замедлить вращение. Чтобы правильно настроить его, нужно просто попробовать разные значения.
Теперь мы можем посчитать вектор, который будет представлять направление в Мировом Пространстве в которое смотрит камера.

// направление : Преобразовываем сферические координаты в декартовы
glm::vec3 direction(
   cos(verticalAngle) * sin(horizontalAngle),
   sin(verticalAngle),
   cos(verticalAngle) * cos(horizontalAngle)
);

Это стандартный способ перехода от сферических координат в декартовы. Если вы ничего не знаете о синусах и косинусах — вот краткое описание:













Формула выше — это то же самое, но в 3д пространстве.
Теперь нам нужно правильно посчитать “up” вектор. Заметьте, «вверх», это совсем не обязательно в сторону +Y. Если вы опустите голову вниз, то ваша макушка будет направлена горизонтально.

Единственная для нас константа за которую можно привязаться — вектор который выходит справа от камеры всегда горизонтальный.
Это можно проверить, если вы выпрямите свою правую руку вбок и начнете крутиться вокруг своей оси, или смотреть вверх и вниз, рука будет горизонтальной.
Давайте объявим вектор «вправо»: его координата Y будет 0, так как он горизонтальный, а координаты X и Z будут вычисляться так:
// Вектор «вправо»
glm::vec3 right = glm::vec3(
   sin(horizontalAngle - 3.14f/2.0f),
   0,
   cos(horizontalAngle - 3.14f/2.0f)
);

Теперь у нас есть вектор «вправо» и вектор «направление». Вектор «вверх» это вектор который перпендикулярный этим двум. Для нахождения перпендикулярных векторов издавна есть прекрасный метод — векторное произведение(cross product)
// Вектор «вверх»: перпендикуляр к направлению и к «вправо»
glm::vec3 up = glm::cross( right, direction );

Чтобы запомнить как работает векторное произведение, просто вспомните правило правой руки из урока 3. Первый вектор, это большой палец, второй вектор — указательный, а средний палец — это и есть векторное произведение. Все очень просто и удобно.

Позиция

Код для вычисления позиции достаточно прост:

// Двигаемся вперед
if (glfwGetKey( GLFW_KEY_UP ) == GLFW_PRESS){
   position += direction * deltaTime * speed;
}
// Двигаемся назад
if (glfwGetKey( GLFW_KEY_DOWN ) == GLFW_PRESS){
   position -= direction * deltaTime * speed;
}
// Шаг вправо
if (glfwGetKey( GLFW_KEY_RIGHT ) == GLFW_PRESS){
   position += right * deltaTime * speed;
}
// Шаг влево
if (glfwGetKey( GLFW_KEY_LEFT ) == GLFW_PRESS){
   position -= right * deltaTime * speed;
}

Единственная вещь на которой я хотел бы заострить внимание — deltaTime. Мы ввели её из-за того, что вы вряд ли бы хотели смещать камеру на 1 каждый кадр, так как:
  • Если у вас быстрый компьютер, и сцена рендерится, например, со скоростью в 100fps, то камера будет перемещаться со скоростью 100 единиц в секунду.
  • Если у вас медленный компьютер, и сцена рендерится, например, со скоростью в 20fps, то камера будет перемещаться со скоростью 20 единиц в секунду.

Чтобы избежать этих проблем с разной скоростью перемещения на разных компьютерах обычно пользуются такой штукой как deltaTime — или время которое прошло с последнего кадра.
  • Если у вас быстрый компьютер, и у вас 100 fps, то вы двигаетесь со скоростью 1/100*скорость за один кадр. Итого 1*скорость в секунду.
  • Если у вас медленны компьютер, и у вас 20 fps, то вы двигаетесь со скоростью 1/20*скорость за один кадр. Итого 1*скорость в секунду.
Это уже гораздо лучше. Сам же deltaTime вычисляется очень просто:

double currentTime = glfwGetTime();
float deltaTime = float(currentTime — lastTime);

Угол обзора

Ради любопытства давайте установим угол обзора зависимым от колесика мышки:

float FoV = initialFoV - 5 * glfwGetMouseWheel();

Вычисление матриц

Вычисление самих матриц очень простое, используем те же функции, что и раньше, только подставляем вычисленные нами параметры:

// Матрица проекции : 45° Угол обзора, 4:3 соотношение сторон, дальность видимости от 0.1  до 100
ProjectionMatrix = glm::perspective(FoV, 4.0f / 3.0f, 0.1f, 100.0f);
// Матрица камеры
ViewMatrix= glm::lookAt(
   position,            // Положение камеры
   position+direction, // А вот сюда мы смотрим
   up
);

Результат


Отсечение невидимых граней

Теперь, когда вы можете свободно двигаться по пространству, можно заметить, что если мы зайдем внутрь куба, то внутренние грани тоже будут видимы. И хотя это может казаться правильным, у нас есть поле для оптимизации. В обычных приложениях мы обычно не входим внутрь объектов.
Идея такова, что мы проверяем положение камеры относительно треугольника. Если камера спереди, то показываем треугольник, если сзади — нет. Так как обычно модели замкнуты, и мы находимся не внутри, то мы и не должны видеть полигоны с обратной стороны. И если не рисовать эти не видимые полигоны, то это может ускорить рендеринг раза в 2.
Самое интересное, что эту проверку очень просто включить. GPU автоматически вычисляет нормаль каждого треугольника(помните векторное произведение?) и проверяет  его направление по отношению к камере.
Один недостаток — ориентация треугольника задана положением его вершин. Это означает, что если вы переставите две вершины в вашем мешбуфере, то у вас будет дырка в кубе. Но обычно никто не создает 3д модели в коде, а делает их в 3д редакторах, а там эта проблема устраняется командой «инвертировать нормали»(на самом деле, как я надеюсь вы уже поняли, инвертируются сами вершины, что в результате и приводит к инвертированию нормалей).

Вот как включить отсечение обратных граней:
// Не показываем треугольники которые смотрят не на камеру
glEnable(GL_CULL_FACE);

Упражнения

  • Ограничьте вертикальный угол, чтобы пользователь не мог двигаться верх и вниз.
  • Создайте камеру которая будет вращаться вокруг объекта(позиция=центрОбъекта+(радиус*cos(время), высота, радиус*sin(время)))


8 комментариев:

  1. Здравствуйте, Админ))
    Не могли бы вы сбросить сюда исходник? (disqrl@yandex.ru)

    ОтветитьУдалить
  2. уроки классные, но действительно нужны исходники..

    ОтветитьУдалить
  3. Исходники можно найти вот тут: http://code.google.com/p/opengl-tutorial-org/source/browse/

    ОтветитьУдалить
  4. Хороши уроки!Жаль Админ не автор!

    ОтветитьУдалить
  5. Этот комментарий был удален автором.

    ОтветитьУдалить
  6. Этот комментарий был удален автором.

    ОтветитьУдалить
  7. Добрый день, в исходном tutorial 'е и тут увидел переменную deltaTime, но в исходном коде controls.cpp её нет, можете объяснить её необходимость? или же это очепятка?

    Теперь вычислим необходимые углы:
    horizontalAngle += mouseSpeed * deltaTime * float(1024/2 - xpos );
    verticalAngle += mouseSpeed * deltaTime * float( 768/2 - ypos );

    ОтветитьУдалить
    Ответы
    1. переменная очень замедляет скорость обзора мышкой

      Удалить