Короткий курс комп'ютерної графіки: пишемо спрощений OpenGL своїми руками, стаття 4a з 6

Зміст курсу


Побудова перспективного спотворення
Четверта стаття буде розбита на дві, перша частина говорить про побудову перспективного викривлення, друга про те, як рухати камеру і що з цього випливає.Завдання на сьогодні навчитися генерувати ось такі картнки:




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



Найпростіше (невырожденное) перетворення задається одиничною матрицею, воно просто залишає кожну точку на місці


Коефіцієнти на діагоналі матриці задають розтягування/стиснення площині. Давайте проілюструємо картинкою: наприклад, якщо ми запишемо наступне перетворення:



То білий об'єкт (квадрат з відрізаним кутом) перетворюється в жовтий. Червоний і зелений відрізки дають одиничні вектори по осі x і y, відповідно.


Всі картинки до цієї статті згенеровані ось цим кодом.

Навіщо взагалі використовувати матриці? Тому що це зручно. Почнемо з того, що в матричній формі перетворення всього об'єкта можна записати таким чином:


Тут перетворення той же, що і в попередньому прикладі, а ось матриця в два рядки і п'ять стовпців не що інше, як масив координат нашого куба з обрізаним кутом. Ми просто взяли цілком масив, помножили на перетворення, і отримали вже перетворений об'єкт. Красиво? Окей, згоден, притягнуто за вуха.

Справжня причина в тому, що вкрай регулярно ми хочемо, щоб об'єкт зазнав кількох перетвореннями поспіль. Уявіть, що ви пишете у вашому коді функції перетворень типу

vec2 foo(vec2 p) return vec2(ax+by, cy+dy);
vec2 bar(vec2 p) return vec2(ex+fy, gy+hy);
[..]
for (each p in object) {
p = foo(bar(p));
}

Цей код робить два лінійні перетворення на кожну вершину об'єкта, а вони обчислюються в мільйонах. І перетворень найчастіше ми хочемо з добрий десяток. Дорого. А з матричним підходом ми перемножуємо всі матриці перетворення і множимо на наш об'єкт один раз. У множенні ми можемо ставити дужки де хочемо, правда ж?

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

Відповідна картинка:

Не що інше, як простий зсув вздовж осі x. Другий анти-діагональний елемент дасть зсув вздовж осі y. Таким чином, базових лінійних перетворень на площині тільки два: розтягування по осях і зсув вздовж осі. Стривайте, скажуть мені, а як же, наприклад, обертання навколо початку координат?

З'ясовується, що обертання може бути представлено як композиція трьох зрушень, тут білий об'єкт перетворено спочатку червоний, потім зелений, потім в синій:



Але не будемо заглиблюватися в крайності, матриця обертання проти годинникової стрілки навколо початку координат може бути записана безпосередньо (пам'ятайте про розстановку дужок?):


Перемножувати ми можемо, звичайно, в будь-якому порядку, тільки давайте не забувати, що для множення матриць некоммутативно:


Що нормально, зрушити і потім повернути (червоний об'єкт) не те ж саме, що спочатку повернути, а потім зрушити (зелений об'єкт):


Аффинные перетворення на площині
Тобто, будь-яке лінійне перетворення на площині це композиція растягиваний і зрушень. Що означає, що якою б не була матриця нашого перетворення, початок координат завжди перейде в початок координат. Таким чином, лінійні перетворення — це прекрасно, але якщо ми не можемо уявити елементарного паралельного перенесення, то наше життя буде сумна. Чи можемо? А що, якщо додати його окремо і записати афінне перетворення як композицію лінійної частини і паралельного переносу? Приблизно ось так:



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


Це починає виглядати вкрай неприємно вже для однієї-єдиної композиції. Спробуйте перетворити це вираз, щоб застосувати до нашого об'єкту тільки одне перетворення виду лінійна частина + паралельний перенос. Особисто мені дуже не хочеться цього робити.

Однорідні координати
А що ж робити? Чаклувати! Уявіть тепер, що я допишу руками один рядок і один стовпець до нашої матриці перетворення і додам третю координату, яка дорівнює одиниці у вектора, який ми перетворюємо:


При множенні цієї матриці 3x3 і нашого вектора, доповненого одиницею, ми знову отримали вектор з одиницею в третій компонент, а решта дві мають той вигляд, який ми хотіли! Колдунство.

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

Як саме ми проектуємо тривимірне простір назад в нашу площину? Дуже просто:


Секундочку, але ж на нуль ділити не можна!
Хто вам сказав? Жарт. Давайте ще раз зрозуміємо, що відбувається.
  • Ми занурюємо наше 2d простір в 3d, зробивши його площиною z=1
  • Робимо що хочемо в 3d
  • Для кожної точки, яку хочемо спроектувати назад в 2d, проводимо пряму між початком координат і цією точкою і шукаємо її перетин з фізичної площиною z=1.
На цій картинці наша фізична площина фіолетова, і точка (x,y,z) проектується в точку (x/z, y/z):



Тепер давайте уявимо вертикальний рейок, що проходить через точку (x,y,1). Куди спроектується точка (x,y,1)? Звичайно ж, в (x,y):


Тепер давайте почнемо ковзати вниз по рейці, наприклад, точка (x, y, 1/2) спроектується в (2x, 2y):


Продовжимо ковзати: точка (x,y,1/4) спроектується в (4x, 4y):


Продовжуючи ковзати до нуля по z, наша проекція йде все далі і далі від центру координат за напрямом (x,y).
Тобто, точка (x,y,0) проектується в нескінченно далеку крапку в напрямку (x,y). А що це? Правильно, це вектор!
Однорідні координати дають можливість розрізняти вектор і точку. Якщо програміст пише vec2 v(x,y), це вектор або точка?
Важко сказати. А в однорідних координатах все, що з нулем по третій компонент, це вектор, все інше кінцеві точки.
Дивіться: вектор + вектор = вектор. Вектор-вектор = вектор. Точка + вектор = точка. Ну не здорово?

Приклад складеного перетворення

Я вже говорив, що нам потрібно вміти накопичувати десятки перетворень. Чому? Припустимо, вам потрібно повернути плоский об'єкт навколо точки (x0, y0). Як це зробити? Можна піти і шукати формули, а можна зробити самим, адже у нас є всі інструменти.
Ми вміємо обертати навколо центру координат, ми вміємо зрушувати. Що ще треба? Зрушуємо x0,y0 в центр координат, обертаємо, повертаємо назад. Халява!



В 3д послідовності дій будуть трохи довше, але зміст залишається тим самим: нам достатньо вміти робити кілька базових перетворень, і з їх допомогою ми можемо закодувати яке завгодно складна.

Стривайте, а чи маю я право чіпати нижню рядок матриці 3x3?

Ще як! Давайте застосуємо ось це перетворення:

до нашого стандартного тестового об'єкту. Нагадую, що тестовий об'єкт білий, поодинокі ікс та ігрек вектора показано червоним і зеленим, відповідно


Ось наш перетворений об'єкт:


І ось тут починається найцікавіше. Пам'ятайте наше вправа про ігрек-буфер? Тут ми будемо робити практично те ж саме.
Ми будемо проектувати наш двовимірний об'єкт на пряму x=0. Причому тепер ускладнимо завдання: проекція буде центральній, наша камера знаходиться в точці (5, 0) і дивиться в початок координат. Щоб знайти проекцію, ми повинні провести прямі, що проходять через точку камери і кожну вершину нашого об'єкта (жовті прямі), а потім знайти їх перетин з прямою екрану (біла вертикальна).



А тепер давайте приберемо оригінальний об'єкт і замість нього намалюємо трансформований.

Якщо ми використовуємо звичайну ортогональну проекцію нашого трансформованого об'єкта, то ми знайдемо рівно ті ж самі точки!
Адже що робить це відображення? Воно кожне вертикальне ребро залишає вертикальним, але при цьому розтягує ті, які близько до камери, і стискає ті, що далі від камери. Правильно підібравши коефіцієнт розтягування-стиснення ми можемо якраз досягти ефекту, що простий ортогональною проекцією ми отримуємо зображення в перспективному спотворенні! У наступному параграфі ми додамо одне вимірювання і покажемо, звідки взявся коефіцієнт -1/5.

Пора перейти до трьом вимірам

Давайте пояснювати тільки що сталася магію.
Як і у випадку двовимірних афінних перетворень у тривимірному просторі ми теж будемо використовувати однорідні координати.
Беремо точку (x,y,z), занурюємо її в чотиривимірний простір, додавши одиницю в четверту компоненту, перетворимо у чотирьох вимірах і проектуємо назад в 3d. Наприклад, візьмемо таке перетворення:



Проекція на 3д дає наступні координати:


Добре запам'ятаємо цей результат, але на пару хвилин його відкладемо. Давайте повернемося до стандартного визначення центральної проекції в звичайному 3д, без однорідних координат та інших екзотичних речей. Нехай у нас буде точка P=(x,y,z), яку ми хочемо спроектувати на площину z=0, камера знаходиться на осі z на відстані c від центру координат.


Ми знаємо, що трикутники ABC і ODC подібні. Тобто, ми можемо записати |AB|/|AC|=|OD|/|OC| => x/(c-z) = x'/c.


Розглядаючи трикутники CPB і CP'D, можна легко прийти до подібної запису і для координати y:


Отже, це дуже-дуже схоже на результат проекції через однорідні координати, тільки там це все вважалося одним матричним множенням. Ми вивели залежність коефіцієнтів r = -1/c.

Для закріплення матеріалу: головна формула на сьогодні
Хоча якщо ви просто візьмете цю формулу, не зрозумівши весь попередній текст, то я вас ненавиджу. Отже, якщо ми хочемо побудувати центральну перспективу з (важливо!) камерою, що знаходиться на осі zвідстані c від початку координат, то спочатку ми занурюємо трехемерные точки в чотиривимірний простір, додавши 1. Потім множимо на наступну матрицю і проектуємо результат назад в 3D:



Ми деформували наш об'єкт так, що тепер для побудови дротяного рендера з перспективою нам досить просто забути про новополученную координату z. Якщо ми хочемо будувати z-буфер, то, зрозуміло, ми її використовуємо. Зліпок код доступний на гітхабі. Результат його роботи видно на самому початку нашої статті.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.