Добро пожаловать на наш шестой урок!

Пришло время узнать, как использовать клавиатуру и мышь, чтобы перемещать камеру также, как и в играх жанра FPS.

Интерфейс

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

Код этого урока будет мало отличаться от предыдущих. Главное отличие состоит в том, что теперь мы будем вычислять MVP матрицу не единожды, а в каждом кадре, поэтому давайте перейдем к коду главного цикла:

 1 do{
 2 
 3     // ...
 4 
 5     // Вычислить MVP-матрицу в зависимости от положения мыши и нажатых клавиш
 6     computeMatricesFromInputs();
 7     glm::mat4 ProjectionMatrix = getProjectionMatrix();
 8     glm::mat4 ViewMatrix = getViewMatrix();
 9     glm::mat4 ModelMatrix = glm::mat4(1.0);
10     glm::mat4 MVP = ProjectionMatrix * ViewMatrix * ModelMatrix;
11 
12     // ...
13 }

Этот отрывок кода имеет 3 новых функции:

  • computeMatricesFromInputs() вычисляет Проекционную и Видовую матрицы в зависимости от текущего ввода. Это та функция, где происходит основная работа.
  • getProjectionMatrix() просто возвращает вычисленную Проекционную матрицу.
  • getViewMatrix() просто возвращает вычисленную Видовую матрицу.

Конечно же, указанный способ - один из немногих которым вы можете следовать.

Теперь перейдем непосредственно к controls.cpp

Основной код

Итак, нам потребуется несколько переменных:

 1 // позиция
 2 glm::vec3 position = glm::vec3( 0, 0, 5 );
 3 // горизонтальный угол
 4 float horizontalAngle = 3.14f;
 5 // вертикальный угол
 6 float verticalAngle = 0.0f;
 7 // поле обзора
 8 float initialFoV = 45.0f;
 9 
10 float speed = 3.0f; // 3 units / second
11 float mouseSpeed = 0.005f;

FoV - это “уровень зума”. 80 = очень широкий угол обзора, сильные деформации. Значение от 60 и до 45 является стандартным. 20 - это сильный зум.

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

Ориентация

Чтение позиции мыши - это просто:

1 // Получить позицию мыши
2 int xpos, ypos;
3 glfwGetMousePos(&xpos, &ypos);

однако, нам важно не забыть о перемещении курсора обратно в центр экрана, чтобы он не выходил за границы окна:

1 // Сбросить позицию мыши для следующего кадра
2 glfwSetMousePos(1024/2, 768/2);

Обратите внимание, что этот код предполагает, что размеры окна - 1024*768, что не всегда будет являться истиной, поэтому лучшим решением будет использовать glfwGetWindowSize().

Теперь мы можем вычислить наши углы:

1 // Вычисляем углы
2 horizontalAngle += mouseSpeed * deltaTime * float(1024/2 - xpos );
3 verticalAngle   += mouseSpeed * deltaTime * float( 768/2 - ypos );

Давайте разберем этот код справа налево:

  • 1024/2 – xpos означает как делеко мышь находится от центра окна. Чем дальше, тем больше будет поворот.
  • float(…) приводит значение в скобках к вещественному типу.
  • mouseSpeed - это скорость, с которой будет происходить поворот (чувствительность мыши).
  • += : Если вы не переместили мышь, то 1024/2 - xpos будет равно 0, значит horizontalAngle+=0 не изменит угол.

We can now compute a vector that represents, in World Space, the direction in which we’re looking

Сейчас нам необходимо вычислить вектор в Мировом пространстве, который будет указывать направление взгляда:

1 // Направление
2 glm::vec3 direction(
3     cos(verticalAngle) * sin(horizontalAngle),
4     sin(verticalAngle),
5     cos(verticalAngle) * cos(horizontalAngle)
6 );

Это стандартное вычисление, но если вы не знаете о синусе и косинусе, то вот небольшая иллюстрация:

Теперь нам необходимо вычислить вектор “up”. То есть вектор, указывает направление вверх для камеры. Обратите внимание, что он не всегда будет равен +Y. К пример, если вы смотрите вниз, то вектор up будет горизонтальным.

В нашем случае, единственное, что остается неизменным - это вектор, который направлен вправо от камеры.

1 // Вектор, указывающий направление вправо от камеры
2 glm::vec3 right = glm::vec3(
3     sin(horizontalAngle - 3.14f/2.0f),
4     0,
5     cos(horizontalAngle - 3.14f/2.0f)
6 );

Итак, у нас есть вектор Вправо и есть направление (вектор Вперед), тогда вектор “вверх” - это вектор, который им перпендикулярен, а чтобы его получить - нужно воспользоваться векторным произведением:

1 // Вектор, указывающий направление вверх относительно камеры
2 glm::vec3 up = glm::cross( right, direction );

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

Позиция

Далее следует совсем простой код. Кстати, я использую клавиши Вверх/Вниз/Влево/Вправо вместо привычных WASD, потому что у меня AZERTY-клавиатура и соответсвенно AWSD на QWERTY клавиатуре = ZQSD на клавиатуре AZERTY. Также, существуют другие раскладки, о которых не стоит забывать. (Подробнее о раскладках можно узнать тут).

 1 // Движение вперед
 2 if (glfwGetKey( GLFW_KEY_UP ) == GLFW_PRESS){
 3     position += direction * deltaTime * speed;
 4 }
 5 // Движение назад
 6 if (glfwGetKey( GLFW_KEY_DOWN ) == GLFW_PRESS){
 7     position -= direction * deltaTime * speed;
 8 }
 9 // Стрэйф вправо
10 if (glfwGetKey( GLFW_KEY_RIGHT ) == GLFW_PRESS){
11     position += right * deltaTime * speed;
12 }
13 // Стрэйф влево
14 if (glfwGetKey( GLFW_KEY_LEFT ) == GLFW_PRESS){
15     position -= right * deltaTime * speed;
16 }

Единственная непонятная вещь в этом коде - это deltaTime. Если мы просто умножим вектор на скорость, то получим неприятные эффекты:

  • Если у вас быстрый компьютер и приложение работает с частотой кадров 60, то вы будете передвигаться со скоростью 60 юнитов в секунду.
  • Если же у вас медленный компьютер и частота кадров = 20, то вы будете передвигаться со скоростью 20 юнитов в секунду.

Таким образом тот, кто имеет быстрый компьютер будет двигаться быстрее, поэтому мы вводим переменную, в которую заносим время, прошедшее с последнего кадра. С помощью GLFW оно вычисляется так:

1 double currentTime = glfwGetTime();
2 float deltaTime = float(currentTime - lastTime);

Поле обзора

Для развлечения мы можем также привязать колесико мышки к переменной FoV и менять таким образом Поле обзора, что в итоге даст нас эдакий зум:

1 float FoV = initialFoV - 5 * glfwGetMouseWheel();

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

Мы уже использовали все функции, приведенные ниже в предыдущих уроках, только теперь мы используем другие параметры:

1 // Проекционная матрица: Поле обзора = FoV, отношение сторон 4 к 3, плоскости отсечения 0.1 и 100 юнитов
2 ProjectionMatrix = glm::perspective(FoV, 4.0f / 3.0f, 0.1f, 100.0f);
3 // Матрица камеры
4 ViewMatrix       = glm::lookAt(
5     position,           // Позиция камеры
6     position+direction, // Направление камеры
7     up                  // Вектор "Вверх" камеры
8 );

Результат

Отсечение задних граней

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

Чтобы не выводить невидимые грани, а соответственно повысить быстродействие нам необходимо проверять где находится камера относительно полигона (спереди или сзади). Хорошая новость в том, что эту проверку очень просто реализовать. GPU должен вычислить нормаль полигона (используя векторное произведение, помните?) и проверяет, как ориентирована нормаль по отношению к камере.

Однако есть один нюанс. Векторное произведение не является коммутативным. Это означает, что порядок, в котором вы умножаете векторы является важным факторов в результате. То есть, если вы перепутаете порядок, то получите неправильную нормаль, а значит не сможете рассчитывать освещение (в дальнейшем) и отсечение невидимых граней будет работать неверно.

Включение режима отсечения полигонов выполняется всего одной командой:

1 // Отсечение тех треугольников, нормаль которых направлена от камеры
2 glEnable(GL_CULL_FACE);

Упражнения

  • Сделайте так, чтобы вы не могли перемещаться вниз или вверх
  • Создайте камеру, которая будет вращаться вокруг заданного объекта. Подсказка:

  • position = ObjectCenter + ( radius * cos(time), height, radius * sin(time) ) );
  • привяжите radius, height, time к клавиатуре

  • Развлекайтесь!