Туторіал 6 : Клавіатура і миша
Ласкаво просимо до нашого 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°, відношення сторін - 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) )
) прив’яжіть радіус/висоту/час до клавіатури/миші чи чого забажаєте. - Розважайтесь!