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

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

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