Урок 6: Клавиатура и мышь
Добро пожаловать на наш шестой урок!
Пришло время узнать, как использовать клавиатуру и мышь, чтобы перемещать камеру также, как и в играх жанра 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 к клавиатуре
- Развлекайтесь!
