Урок 6: Клавиатура и мышь
Добро пожаловать на наш шестой урок!
Пришло время узнать, как использовать клавиатуру и мышь, чтобы перемещать камеру также, как и в играх жанра FPS.
Интерфейс
Так как код этого урока будет использоваться в дальнейшем, мы поместим его в отдельный файл common/controls.cpp и объявим необходимые функции в common/controls.hpp, таким образом tutorial06.cpp будет их видеть.
Код этого урока будет мало отличаться от предыдущих. Главное отличие состоит в том, что теперь мы будем вычислять MVP матрицу не единожды, а в каждом кадре, поэтому давайте перейдем к коду главного цикла:
do{
// ...
// Вычислить MVP-матрицу в зависимости от положения мыши и нажатых клавиш
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() просто возвращает вычисленную Видовую матрицу.
Конечно же, указанный способ - один из немногих которым вы можете следовать.
Теперь перейдем непосредственно к controls.cpp
Основной код
Итак, нам потребуется несколько переменных:
// позиция
glm::vec3 position = glm::vec3( 0, 0, 5 );
// горизонтальный угол
float horizontalAngle = 3.14f;
// вертикальный угол
float verticalAngle = 0.0f;
// поле обзора
float initialFoV = 45.0f;
float speed = 3.0f; // 3 units / second
float mouseSpeed = 0.005f;
FoV - это “уровень зума”. 80 = очень широкий угол обзора, сильные деформации. Значение от 60 и до 45 является стандартным. 20 - это сильный зум.
В первую очередь мы будем вычислять позицию, горизонтальный и вертикальный углы, а также FoV опираясь на ввод, после чего вычислим Видовую и проекционную матрицы.
##Ориентация
Чтение позиции мыши - это просто:
// Получить позицию мыши
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 – 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
Сейчас нам необходимо вычислить вектор в Мировом пространстве, который будет указывать направление взгляда:
// Направление
glm::vec3 direction(
cos(verticalAngle) * sin(horizontalAngle),
sin(verticalAngle),
cos(verticalAngle) * cos(horizontalAngle)
);
Это стандартное вычисление, но если вы не знаете о синусе и косинусе, то вот небольшая иллюстрация:
Теперь нам необходимо вычислить вектор “up”. То есть вектор, указывает направление вверх для камеры. Обратите внимание, что он не всегда будет равен +Y. К пример, если вы смотрите вниз, то вектор up будет горизонтальным.
В нашем случае, единственное, что остается неизменным - это вектор, который направлен вправо от камеры.
// Вектор, указывающий направление вправо от камеры
glm::vec3 right = glm::vec3(
sin(horizontalAngle - 3.14f/2.0f),
0,
cos(horizontalAngle - 3.14f/2.0f)
);
Итак, у нас есть вектор Вправо и есть направление (вектор Вперед), тогда вектор “вверх” - это вектор, который им перпендикулярен, а чтобы его получить - нужно воспользоваться векторным произведением:
// Вектор, указывающий направление вверх относительно камеры
glm::vec3 up = glm::cross( right, direction );
Чтобы запомнить что делает векторное произведение попробуйте вспомнить Правило правой руки из Урока 3. Первый вектор - это большой палец; Второй вектор - это указательный палец; Результатом будет являться ваш средний палец.
##Позиция
Далее следует совсем простой код. Кстати, я использую клавиши Вверх/Вниз/Влево/Вправо вместо привычных WASD, потому что у меня AZERTY-клавиатура и соответсвенно AWSD на QWERTY клавиатуре = ZQSD на клавиатуре AZERTY. Также, существуют другие раскладки, о которых не стоит забывать. (Подробнее о раскладках можно узнать тут).
// Движение вперед
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. Если мы просто умножим вектор на скорость, то получим неприятные эффекты:
- Если у вас быстрый компьютер и приложение работает с частотой кадров 60, то вы будете передвигаться со скоростью 60 юнитов в секунду.
- Если же у вас медленный компьютер и частота кадров = 20, то вы будете передвигаться со скоростью 20 юнитов в секунду.
Таким образом тот, кто имеет быстрый компьютер будет двигаться быстрее, поэтому мы вводим переменную, в которую заносим время, прошедшее с последнего кадра. С помощью GLFW оно вычисляется так:
double currentTime = glfwGetTime();
float deltaTime = float(currentTime - lastTime);
##Поле обзора
Для развлечения мы можем также привязать колесико мышки к переменной FoV и менять таким образом Поле обзора, что в итоге даст нас эдакий зум:
float FoV = initialFoV - 5 * glfwGetMouseWheel();
##Вычисление матриц
Мы уже использовали все функции, приведенные ниже в предыдущих уроках, только теперь мы используем другие параметры:
// Проекционная матрица: Поле обзора = FoV, отношение сторон 4 к 3, плоскости отсечения 0.1 и 100 юнитов
ProjectionMatrix = glm::perspective(glm::radians(FoV), 4.0f / 3.0f, 0.1f, 100.0f);
// Матрица камеры
ViewMatrix = glm::lookAt(
position, // Позиция камеры
position+direction, // Направление камеры
up // Вектор "Вверх" камеры
);
Результат
##Отсечение задних граней
Теперь вы можете свободно двигаться вокруг и должны были заметить, что если вы попадаете внутрь куба, то полигоны все равно выводятся. Это кажется нормальным, но в тоже время открывает нам возможность оптимизации, так как в обычных приложениях вы никогда не находитесь внутри куба.
Чтобы не выводить невидимые грани, а соответственно повысить быстродействие нам необходимо проверять где находится камера относительно полигона (спереди или сзади). Хорошая новость в том, что эту проверку очень просто реализовать. GPU должен вычислить нормаль полигона (используя векторное произведение, помните?) и проверяет, как ориентирована нормаль по отношению к камере.
Однако есть один нюанс. Векторное произведение не является коммутативным. Это означает, что порядок, в котором вы умножаете векторы является важным факторов в результате. То есть, если вы перепутаете порядок, то получите неправильную нормаль, а значит не сможете рассчитывать освещение (в дальнейшем) и отсечение невидимых граней будет работать неверно.
Включение режима отсечения полигонов выполняется всего одной командой:
// Отсечение тех треугольников, нормаль которых направлена от камеры
glEnable(GL_CULL_FACE);
Упражнения
- Сделайте так, чтобы вы не могли перемещаться вниз или вверх
-
Создайте камеру, которая будет вращаться вокруг заданного объекта. Подсказка:
- position = ObjectCenter + ( radius * cos(time), height, radius * sin(time) ) );
-
привяжите radius, height, time к клавиатуре
- Развлекайтесь!