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

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

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