Ласкаво просимо до нашого 6 туторіалу !

Тепер ми вивчимо як використовувати мишку та клавіатуру для переміщення камери як шутерах.

Інтерфейс

так як цей код скоріше за все буде використовуватись в подальших туторіалах, ми розмістимо цей код в окремому файлі - common/controls.cpp і об’явимо функції в common/controls.hpp так що б tutorial06.cpp знав про них.

Код 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() читає дані з клавіатури та миші і обчислює матриці Projection та View. Це місце, де трапляється вся магія.
  • getProjectionMatrix() просто повертає обчислену матрицю Projection.
  • getViewMatrix() просто повертає матрицю View.

Це один з способів це зробити. Якщо Вам не подобаються ці функції - просто змініть їх.

Давайте поглянемо, що всередині controls.cpp.

Код в середині

Нам потрібно декілька змінних.

// позиція
glm::vec3 position = glm::vec3( 0, 0, 5 );
// кут по горизонталі : в напрямку -Z
float horizontalAngle = 3.14f;
// кут по вертикалі : 0, якщо дивитись на горизонт
float verticalAngle = 0.0f;
// Початкове поле зору
float initialFoV = 45.0f;

float speed = 3.0f; // 3 одиниці / секунду
float mouseSpeed = 0.005f;

FoV - це рівень збільшення. 80° - дуже широкий кут огляду, великі деформації. 60° - 45° - стандартне значення. 20° - велике збільшення.

Спочатку ми розрахуємо позицію, horizontalAngle (кут по горизонталі), verticalAngle (кут по вертикалі) та 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 буде рівним нулю. І horizontalAngle+=0 не змінить horizontalAngle. Якщо Ви будете використовувати =, Ви будете постійно повертатись на початкове положення кожний кадр, що не є добре.

Тепер ми можемо розрахувати вектор, який показує напрямок погляду в світових координатах.

// Напрямок - сферичні координати перетворюємо в декартові
glm::vec3 direction(
    cos(verticalAngle) * sin(horizontalAngle),
    sin(verticalAngle),
    cos(verticalAngle) * cos(horizontalAngle)
);

Це стандартні розрахунки, але якщо Ви нічого не знаєте про синуси і косинуси, ось невелике пояснення:

ФОрмули вище є просто узагальненням для 3Д

Тепер ми хочемо обчислити вектор “вгору” правильно. Зверніть увагу, що “вгору” не завжди має напрямок паралельний +Y - якщо, до прикладу, Ви дивитесь вниз, то вектор “вгору” буде направлений горизонтально. Ось приклад камер, що знаходяться в одній і тій же позиції, в тому же напрямку, але з різними “вгору”:

В нашому випадку, єдина константа це вектор, що йде праворуч від камери і він завжди горизонтальний. Ви можете перевірити це поставивши Вашу руку горизонтально і подивитись вгору/вниз/будь-куди. Давайте дамо визначення вектору “вправо” - його Y координата дорівнює нулю, так як він горизонтальний, і його X та Z координати такі як і у фігури вище, але повернуті на 90° або Pi/2 радіан.

// вектор "вправо"
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. Перший вектор - це великий палець, другий вектор - вказівний і результат - середній палець. Дуже зручно.

Позиція

Код достатньо прямолінійний. Зауважте, що я використовую стрілочки вгору/вниз/праворуч/ліворуч, не awsd, тому що у мне azerty клавіатура (німецька) і awsd у мене zqsd. Також, це буде по своєму на qwerZ клавіатурах, давайте залишимо корейську клавіатуру в стороні - я не знаю, як у них розташовані клавіші, але думаю там все по своєму.

// рухаємося вперед
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*speed одиниць за секунду
  • Якщо у Вас повільний комп’ютер і у Вас 20 кадрів на секунду, Ви будете переміщуватись 20*speed одиниць за секунду

Ті хто мають швидший комп’ютер не завжди хочуть рухатись швидше, потрібно масштабувати відстань на основі “час відносно останнього кадра”, або “дельта часу”, що є значно краще. дельта часу (deltaTime) дуже легко порахувати:

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

Поле зору

Заради розваги, ми можемо прив’язати колесо миші до поля зору - у нас буде “дешеве збільшення/зменшення”:

float FoV = initialFoV - 5 * glfwGetMouseWheel();

Розрахунки матриць

Обчислення матриць дуже просте. Ми використовуємо ті самі функції, що і раніше, але з новими параметрами.

// Матриця проекції - поле зору 45&deg, відношення сторін - 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                  // "голова зверху" (використовуйте значення 0,-1,0 що б дивитись догори дриґом)
);

Результат

Відсікання задньої поверхні (Backface Culling)

Тепер ми можемо вільно переміщуватись навколо і якщо ми заходимо всередину куба, полігони все ще відображаються. Це здається очевидним, але це можливість до оптимізацій. В реальному додатку ми ніколи не будемо в середині куба.

Ідея оптимізації в тому, що б дозволити відеокарті перевіряти, камера перед чи за трикутником. Якщо камера перед трикутником - відображаємо його, якщо камера за трикутником і меш “закритий” i ми не в середині меша, тоді повинен бути інший трикутник перед ним і ніхто не побачить нічого, проте все буде швидше - в двічі менше трикутників для малювання!

І найкраще те, що це дуже легко перевірити. Відеокарта розраховує нормаль трикутника (пам’ятаєте векторний добуток?) і перевіряє, чи орієнтована ця нормаль в напрямку камери.

Це звичайно має певну ціну - орієнтація трикутника неявна. Це значить, що якщо Ви обміняєте дві вершини місцями в буфері, то це все закінчиться діркою. Але це вартує невеликої додаткової роботи. Зазвичай Ви просто можете клікнути мишкою по “інвертувати нормалі” в редакторі 3д моделі (що інвертує вершини (обміняє місцями) і як наслідок, нормалі) і все буде виглядати добре.

Ввімкнення відсікання - це дуже просто:

// Відсікання трикутників, нормалі яких не направлені на камеру
glEnable(GL_CULL_FACE);

Вправи

  • Обмежте verticalAngle, так що б було неможливо рухатись вгору-вниз
  • Створіть камеру, яка буде рухатись навколо об’єкта (position = ObjectCenter + ( radius * cos(time), height, radius * sin(time) )) прив’яжіть радіус/висоту/час до клавіатури/миші чи чого забажаєте.
  • Розважайтесь!