Двигун не рухає корабель зовсім. Корабель стоїть на місці, а двигуні переміщую всесвіт навколо.

Futurama

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

Однорідні координати

wiki

До тепер ми думали про точки в 3-х вимірному просторі як про (x,y,z). Давайте додамо w. Тепер у нас буде вектор (x,y,z,w).

Це буде зрозуміліше скоро, але зараз запам’ятайте наступне:

  • Якщо w == 1, тоді вектор (x,y,z,1) позиція в просторі.
  • Якщо w == 0, тоді вектор (x,y,z,0) напрямок.

(А ще краще запам’ятати це назавжди.)

Що це змінює? Гаразд, для поворотів це не змінює нічого. Коли ми крутимо точку чи напрямок ми отримуємо те ж сами. Але для переміщення (коли точка переміщується в певному напрямку), дещо змінюється. А що може означати “перемістити напрямок”? Не багато.

Однорідні координати дозволять нам використовувати однакові математичні формули для цих двох випадків.

Матриця переходу

wiki

An introduction to matrices

Матриця це просто масив з певною кількістю стовпчиків та рядків. Наприклад, матриця 2х3 може виглядати так:

В 3D графіці ми в основному використовуємо матриці 4х4. Вони дозволяють нам трансформувати наші вершини (x,y,z,w). Це досягається множенням вершини на матрицю :

Matrix x Vertex (in this order !!) = TransformedVertex

Це не так страшно, як це виглядає. Поставте свій лівий палець на a, ваш правий палець на x. Це ax. Перемістіть ваш лівий палець до наступного числа (b) і ваш правий палець до наступного числа (y). Тепер ви маєте by. І ще раз : cz. Ще раз : dw. ax + by + cz + dw. Ви отримали ваш новий x!. Зробіть це для кожного рядка і ви отримаєте новий вектор (x,y,z,w).

Зараз це трішки нудно обчислювати, ми будемо робити це часто, то ж попросимо комп’ютер робити це за нас.

На C++, з GLM:

glm::mat4 myMatrix;
glm::vec4 myVector;
// заповнимо myMatrix і myVector чимось
glm::vec4 transformedVector = myMatrix * myVector; // Знову, в цьому порядку ! Це дуже важливо.

В GLSL :

mat4 myMatrix;
vec4 myVector;
// заповнимо myMatrix і myVector чимось
vec4 transformedVector = myMatrix * myVector; // Так, це достатньо схоже на  GLM

( чи спробували ви скопіювати це в ваш код? давайте, спробуйте )

Матриця перенесення

Це сама проста матрична трансформація для розуміння. Матриця перенесення виглядає так :

де X,Y,Z є значеннями, які ви хочете додати до вашої позиції

То ж, якщо ви хочете перенести вектор (10,10,10,1) на 10 одиниць по осі X, отримуємо :

(зробіть це! ЗРОБІТЬ)

… і ми отримуємо однорідний вектор (20,10,10,1) ! Пам’ятайте, 1 означає, що є позиція, не напрямок. Наша трансформація не змінила того факту, о ми мали справу з позицією і це дуже добре.

Давайте поглянемо, що трапиться з вектором, який показує напрямок по осі -z : (0,0,-1,0)

… отримали оригінальний напрямок (0,0,-1,0), що є добре, тому що як я говорив раніше, переміщення напрямку позбавлено жодного сенсу.

А як же це перевести в код?

В C++, з GLM:

#include <glm/gtx/transform.hpp> // після <glm/glm.hpp>
 
glm::mat4 myMatrix = glm::translate(glm::mat4(), glm::vec3(10.0f, 0.0f, 0.0f));
glm::vec4 myVector(10.0f, 10.0f, 10.0f, 0.0f);
glm::vec4 transformedVector = myMatrix * myVector; // вгадайте результат

В GLSL :

vec4 transformedVector = myMatrix * myVector;

Добре, насправді, ви практично ніколи не будете робити це в GLSL. В більшості випадків, ви будете використовувати функцію glm::translate() в C++ що б розрахувати матрицю і надіслати її в GLSL і зробити тільки множення :

TODO: тут повинен бути малюнок множення

Одинична матриця

wiki

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

В C++ :

glm::mat4 myIdentityMatrix = glm::mat4(1.0f);

Матриці масштабування

Матриці масштабування теж не складні :

Отже, якщо ви хочете збільшити вектор (положення чи напрямок, не має значення) на 2.0 в всіх напрямках :

і w знову не змінюється. Ви можете запитати - в чому суть “масштабування напрямку”?. Добре, зазвичай це позбавлено сенсу і ви не будете використовувати подібне, проте в деяких (дуже рідкісних) випадках це може бути зручним.

(чи помітили ви, що одинична матриця це просто спеціальний випадок матриці масштабування де (X,Y,Z) = (1,1,1). Це також особливий випадок матриці перенесення де (X,Y,Z)=(0,0,0))

In C++ :

// Use #include <glm/gtc/matrix_transform.hpp> and #include <glm/gtx/transform.hpp>
glm::mat4 myScalingMatrix = glm::scale(2.0f, 2.0f ,2.0f);

Матриці повороту

Це трішки складнувато. Я не буду заглиблюватись в деталі, так як це не так важливо. Якщо вам цікаво, почитайте ЧАПИ: матриці і кватерніони (достатньо популярний ресурс, можливо доступний вашою мовою). Також можна подивитись туторіал по поворотам

В C++ :

// Використовуємо #include <glm/gtc/matrix_transform.hpp> і #include <glm/gtx/transform.hpp>
glm::vec3 myRotationAxis( ??, ??, ??);
glm::rotate( angle_in_degrees, myRotationAxis );

Поєднання перетворень

Тепер ви знаєте, як робити повороти, перенесення та масштабування векторів. Було б добре вміти поєднувати ці перетворення. Цього можна досягти, перемноживши матриці разом, наприклад :

TransformedVector = TranslationMatrix * RotationMatrix * ScaleMatrix * OriginalVector;

!!! ОБЕРЕЖНО !!! Ці рядки коду насправді спочатку масштабують, потім повертають і в кінці вже роблять перенесення.Так працюють матричні перетворення.

Якщо написати ці операції в іншому порядку, то швидше за все результат буде іншим. Спробуйте :

  • зробіть крок вперед (обережно, там ваш комп’ютер) і поверніть наліво ;

  • поверніть наліво і зробіть крок вперед;

Зрозуміло, що порядок вище це те, що вам зазвичай потрібно для ігрових героїв та об’єктів : спочатку масштабуйте, потім задайте напрямок і робіть перенесення. Наприклад, у нас є модель корабля (повороти не використовуємо для спрощення) :

  • Невірний варіант :
    • Ви переносите корабель на (10,0,0). Тепер його центр на 10 одиниць зміщено відносно оригінального положення
    • Ви масштабуєте ваш корабель в 2 рази. Всі координати тепер помножено на 2 відносно оригінального положення, яке зараз далеко звідси… І тепер ви маєте великий корабель, і його центр знаходиться на відстані 2*10 = 20 одиниць. Це не те, що ви хочете.
  • Правильний варіант.
    • Ви збільшуєте ваш корабель в 2 рази. Ви отримуєте великий корабель, а його центр залишається на попередньому місці.
    • Ви переносите його. Його розмір не змінюється і він на правильній дистанції.

Перемноження матриць дуже схоже до перемноження матриці на вектор. Тому ми можемо пропустити це знову і якщо вам цікаво, завітайте на сторінку ЧАПИ: матриці і кватерніони. Зараз ми просто попросимо комп’ютер зробити це за нас :

В C++, за допомогою GLM :

glm::mat4 myModelMatrix = myTranslationMatrix * myRotationMatrix * myScaleMatrix;
glm::vec4 myTransformedVector = myModelMatrix * myOriginalVector;

В GLSL :

mat4 transform = mat2 * mat1;
vec4 out_vec = transform * in_vec;

Матриці моделі, виду та проекції

Далі в цьому туторіалі ми припускаємо, що ви знаєте, як малювати в Blender популярну 3d модель - мавпу Сюзанну (Suzanne).

Матриці моделі, вита та проекції зручні інструменти для розділення перетворень на частини. Ми можете не використовувати їх (в кінці кінців, ми робили це в туторіалі 1 та 2). Але ви повинні. Це те, як всі це роблять, тому що це найпростіший спосіб.

Матриця моделі

Ця модель, як і наш улюблений червоний трикутник, визначена набором вершин. Координати X,Y,Z цих вершин визначені відносно центра об’єкта, тобто це так, начебто (0,0,0) є центром моделі.

Нам би хотілось мати змогу переміщувати цю модель, тому що гравець контролює її за допомогою клавіатури та мишки. Це легко, так як ви вже навчились робити це перенесення*поворот*масштаб. Ви просто застосовуєте цю матрицю до кожної вершини в кожному кадрі (в GLSL, не в C++!) і все рухається. Те що не переміщується буде знаходитись в центрі світу.

Your vertices are now in World Space. This is the meaning of the black arrow in the image below : We went from Model Space (all vertices defined relatively to the center of the model) to World Space (all vertices defined relatively to the center of the world).

Ми можемо підсумувати це наступною діаграмою :

Матриця виду

Ще одна цитата з Футурами :

Двигун не переміщую корабель зовсім. Корабель стоїть на місці, а двигун переміщую всесвіт навколо.

Якщо трішки поміркувати, то те саме можна застосувати і до камер. Якщо ви хочете побачити гору під іншим кутом, ви можете перемістити камеру… чи перемістити гору. Це не зовсім практично в реальному житті, але в комп’ютерній графіці це може бути простіше.

Отже, спочатку ваша камера знаходиться на початку Світових координат. Для того, щоб перемістити світ, ми просто додамо ще одну матрицю. Нехай ви хочете перемістити вашу камеру на 3 одиниці вправо (+X). Це еквівалентно переміщенню всього світу (з сіткою) на 3 одиниці вліво (-X). Доки ваші мізки горять, зробимо наступне :

// використовуємо #include <glm/gtc/matrix_transform.hpp> і #include <glm/gtx/transform.hpp>
glm::mat4 ViewMatrix = glm::translate(glm::mat4(), glm::vec3(-3.0f, 0.0f ,0.0f));

Ще раз, зображення вище ілюструє наступне : ми перемістились з світового простору (всі вершини визначені відносно центру світу, як ми це зробили в попередній секції) в простір камери (всі вершини визначені відносно камери).

А поки ваша голова не вибухнула від цього, повеселімося з чудовою функцією glm::lookAt з GLM :

glm::mat4 CameraMatrix = glm::lookAt(
    cameraPosition, // позиція камери в світових координатах
    cameraTarget,   // куди ви хочете дивитись, в світових координатах
    upVector        // швидше за все просто glm::vec3(0,1,0), але (0,-1,0) якщо бажаєте подивитись, як воно буде догори дриґом
);

Ось обов’язкова схема

Але це ще далеко не все.

Матриця проекції

Тепер ми в просторі камери. Це значить, що після всіх трансформацій, вершина яка має координати x==0 та y==0 буде намальована в центрі екрана. Але двох координат недостатньо для цього. Нам ще потрібна відстань до камери (z). Для двох вершин з однаковими координатами x та y, вершина з більшою координатою z буде більше в центрі екрана (буде більшою), ніж інша.

Це називається перспективною проекцією :

І на щастя для нас, матриця 4х4 може відобразити цю проекцію1:

// Генерація складної для прочитання матриці, але це звичайна 4х4 матриця
glm::mat4 projectionMatrix = glm::perspective(
    glm::radians(FoV), // Вертикальне поле зору (FoV), в радіанах. Міра збільшення. Це як лінзи камери. Зазвичай лежить в межах 90° (дуже широке) і 30° (трішки збільшене)
    4.0f / 3.0f,       //Співвідношення сторін. Залежить від розміру вашого вікна. Зауважте, що 4/3 == 800/600 == 1280/960 - знайомо?
    0.1f,              // Ближня площина відсікання. Намагайтесь використовувати якомога більше значення, інакше будуть проблеми с точністю.
    100.0f             // Дальня площина відсікання. Намагайтесь використовувати якомога менше значення.
);

І в останнє :

Ми перейшли з простору камери (всі вершини визначені відносно камери) в Однорідний простір (всі вершини визначені в маленькому кубі. Все що в середині куба - видно на екрані).

І фінальна діаграма :

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

Множення всього на матрицю проекції має наступний ефект :

В цьому зображенні піраміда камери стала ідеальним кубом (з координатами від -1 до 1 по всім осям, це трішки складно, щоб побачити), і всі сині об’єкти були деформовані однаковим чином. Таким чином, об’єкти, що знаходяться ближче до камери (ближче передньої грані куба) будуть великими, інші будуть меншими. Прямо як в реальному житті !

Давайте поглянемо, як це виглядає з сторони камери :

Ось ваше зображення ! Це зображення трішки квадратне, тому що були застосовані інші перетворення (вони автоматичні, вам не потрібно нічого робити для цього в шейдерах), що б зображення гарно виглядало в реальному вікні : :

І це зображення, яке насправді буде намальовано !

Комулятивні перетворення : Матриця МодельВидПроекція

… Звичайні стандартні матричні множення, так як ви їх любите !

// C++ : розраховуємо матрицю
glm::mat4 MVPmatrix = projection * view * model; // Пам'ятайте - зворотній порядок !
// GLSL : застосуємо це
transformed_vertex = MVP * in_vertex;

Зберемо все разом

  • Крок перший : додамо функцію трансформації з GLM GTC :
#include <glm/gtc/matrix_transform.hpp>
  • Другий крок : згенеруємо нашу MVP матрицю. Це потрібно зробити для кожної моделі, яку ви хочете намалювати.

    // Матриця проекції : поле зору : 45°, співвідношення 4:3, діапазон : 0.1 одиниць <-> 100 одиниць
    glm::mat4 Projection = glm::perspective(glm::radians(45.0f), (float) width / (float)height, 0.1f, 100.0f);
      
    // Для ортокамери  :
    //glm::mat4 Projection = glm::ortho(-10.0f,10.0f,-10.0f,10.0f,0.0f,100.0f); // В світових координатах
      
    // Camera matrix
    glm::mat4 View = glm::lookAt(
        glm::vec3(4,3,3), // Камера знаходиться в координатах (4,3,3), в світових координатах
        glm::vec3(0,0,0), // І направлена на початок координат
        glm::vec3(0,1,0)  // "голова зверху" (можна використати 0,-1,0 для догори дриґом)
        );
      
    // Матриця моделі : одинична матриця (модель буде в центрі координат)
    glm::mat4 Model = glm::mat4(1.0f);
    // Наша матриця МодельВидПроекція : перемножимо наші три матриці
    glm::mat4 mvp = Projection * View * Model; // Пам'ятайте, матриці потрібно множити в зворотному порядку
    
  • Третій крок : передамо це в GLSL

    // Отримаємо об'єкт "MVP" uniform
    // Тільки під час ініціалізації
    GLuint MatrixID = glGetUniformLocation(programID, "MVP");
      
    // Відправимо нашу трансформацію до поточного обраного шейдера, в "MVP" uniform
    // Це потрібно робити в головному циклі, так як кожна модель буде мати свою  MVP матрицю (як мінімум для частини M - модель)
    glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &mvp[0][0]);
    
  • Четвертий крок : використовуємо в GLSL для перетворення наших вершин в SimpleVertexShader.vertexshader

    // Вхідні вершини, різні для всіх виконань цього шейдера.
    layout(location = 0) in vec3 vertexPosition_modelspace;
      
    // Значення, що залишається незмінним для всієї сітки (меша)
    uniform mat4 MVP;
      
    void main(){
      // вивід позиції вершини, в обрізаному просторі : MVP * position
      gl_Position =  MVP * vec4(vertexPosition_modelspace,1);
    }
    
  • Готово ! У нас такий же трикутник, як і в туторіалі номер 2, все ще в початку координат (0,0,0), але ми бачимо його з точки (4,3,3), голова камери направлена догори і поле зору 45°

В туторіалі номер 6, ви вивчите як модифікувати ці значення динамічно, використовуючи клавіатуру/мишку, що б створити камеру як в іграх, але спочатку ми вивчимо як розфарбувати нашу модель певним кольором (туторіал 4) і накладемо зображення/текстуру (туторіал 5).

Вправи

  • Спробуйте змінити glm::perspective
  • Спробуйте замість перспективної проекції використати ортографічну проекцію (glm::ortho)
  • Модифікуйте матрицю моделі для перенесення, повороту та масштабування.
  • Зробіть це саме, тільки в іншому порядку. Що ви помітили? Який порядок буде найкращим для переміщень “героя”?

Додаток

  1. […] на щастя для нас, матриця 4x4 може відобразити цю проекцію : насправді це не так. Трансформація перспективи не є афінною, і як наслідок, не може повністю бути реалізована матрицями. Після множення на матрицю проекції, однорідні координати є поділеними на їх компоненту W. Ця компонента W буває рівною -Z (тому що матриця проекції так зроблена). І тому, точки, що далеко від центру координат будуть поділені на велике Z, їх X та Y координати будуть маленькими, точки будуть більш близькими одна до другої, об’єкти більш маленькими це все дає перспективу. Ця трансформація виконується апаратно (залізом) і не видна в шейдерах.