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

Зміст курсу


Сьогодні ми закінчуємо з лікнепом з геометрії, в наступний раз веселощі з шейдерами!
Щоб не було зовсім нудно, от вам тонування Гуро:

Я прибрав текстури, щоб було видніше. Тонування Гуро дуже проста: добрий дядечко-моделер дав нам нормальні вектора до кожної вершини об'єкта, вони зберігаються в рядках vn x y z файлу .obj. Ми вважаємо інтенсивність освітлення для кожної вершини трикутника і просто интерполируем інтенсивність всередині. Рівно як ми робили для глибини z або для текстурних координат uv!

До речі, якби дядько-моделер був не таким добрим, то ми могли б порахувати нормалі до вершини як середнє нормалей граней, прилеглих до цієї вершини.

Поточний код, який згенерував цю картинку, перебуває тут.


Лікнеп: зміна базису в тривимірному просторі
В евклідовому просторі система координат (репер) задається точкою відліку і базисом простору. Що означає, що в репере (O, i,j,k) точка P має координати (x,y,z)? Це означає, що вектор OP задається наступним чином:


Тепер уявімо, що у нас є другий репер (O',i',j',k'). Як нам перетворити координати точки, дані в одному репере, в іншій репер? Для початку зауважимо, що, так як (i,j,k) та (i',j',k') — це базиси, то існує невироджена матриця М, така що:


Давайте намалюємо ілюстрацію, щоб було наочніше:


Розпишемо подання вектора OP:

підставимо у другу частину вираз заміни базису:


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


Пишемо свій gluLookAt
OpenGL і, як наслідок, наш маленький рендерер вміють малювати сцени лише з камерою, що знаходиться на осі z. Якщо нам потрібно посунути камеру, нічого страшного, ми просто посунемо всю сцену, залишивши камеру нерухомою.

Давайте поставимо задачу наступним чином: ми хочемо зробити так, щоб камера знаходилася в точці e (eye), дивилася в точку c (center) і щоб заданий вектор u (up) у нашій фінальної картинці був би вертикальний.

Ось ілюстрація:

Це просто означає, що ми робимо рендер в репере (c, 'y'z'). Але ж модель задана в репере (O, xyz), значить, нам потрібно порахувати репер 'y'z' і відповідну матрицю переходу. Ось код, який повертає потрібну нам матрицю:

Matrix lookat(Vec3f eye, Vec3f center, Vec3f up) {
Vec3f z = (eye-center).normalize();
Vec3f x = (up^z).normalize();
Vec3f y = z^x;
Matrix res = Matrix::identity(4);
for (int i=0; i < 3; i++) {
res[0][i] = x[i];
res[1][i] = y[i];
res[2][i] = z[i];
res[3][i] = center[i];
}
return res;
}

Почнемо з того, що z' — це просто вектор ce (не забудемо його нормалізувати, так простіше працювати). Як порахувати x'? Просто векторним добутком між u та z'. Потім вважаємо y', який буде ортогонален вже посчитанным x' і z' (нагадую, що за умовою задачі вектор ce та u не обов'язково ортогональны). Самим останнім акордом робимо паралельний перенесення у c і наша матриця перерахунку координат готова. Достатньо взяти будь-яку точку з координатами (x,y,z,1) в старому базисі, помножити її на цю матрицю, і ми отримаємо координати в новому базисі! В OpenGL ця матриця називається матрицею виду (view matrix).

Viewport
Якщо ви пам'ятаєте, то в мене в коді зустрічалися подібні конструкції:

screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);

Що це означає? У мене є точка Vec2f v, яка належить квадрату [-1,1]*[-1,1]. Я хочу її намалювати на картинці розміром (width, height). Вектор (v.x+1) змінюється в межах від 1 до 2, (v.x+1.)/2. в межах від нуля до одиниці, ну а (v.x+1.)*width/2. замітає всю картинку, що мені і треба.

Але ми переходимо до матричному подання афінних відображень, тому давайте розглянемо наступний код:

Matrix viewport(int x, int y, int w, int h) {
Matrix m = Matrix::identity(4);
m[0][3] = x+w/2.f;
m[1][3] = y+h/2.f;
m[2][3] = depth/2.f;

m[0][0] = w/2.f;
m[1][1] = h/2.f;
m[2][2] = depth/2.f;
return m;
}

Він будує ось таку матрицю:

Це означає, що куб світових координат[-1,1]*[-1,1]*[-1,1] відображається в куб екранних координат (так, куб, оскільки у нас є z-буфер!) [x,x+w]*[y,y+h]*[0,b], де d — це здатність z-буфера (у мене 255, т. к. я бережу його безпосередньо в чорно-білій картинці).

У світі OpenGL ця матриця називається viewport matrix.

Ланцюг перетворень
Отже, резюмуємо. Моделі (наприклад, пресонажи) зроблені в своїй локальній системі координат (object coordinates). Вони вставляються в сцену, яка виражена у світових координатах (world coordinates). Перехід від одних до інших здійснюється матрицею Model. Далі, ми хочемо висловити це справа в репере камери (eye coordinates), матриця переходу від світових до камери називається View. Потім, ми здійснюємо перспективне спотворення за допомогою матриці Projection (див. статтю 4а), вона переводить сцену в так звані clip coordinates. Ну і потім ми відображаємо це все справа на екрані, матриця переходу до екранними координатами це Viewport.

Тобто, якщо ми прочитали точку v з файлу, щоб показати її на екрані, ми робимо множення
Viewport * Projection * View * Model * v.

Якщо подивитися в код на гітхабі, то ми побачимо такі рядки:

Vec3f v = model->vert(face[j]);
screen_coords[j] = Vec3f(ViewPort*Projection*ModelView*Matrix(v));

Так як я малюю тільки один об'єкт, то матриця Model у мене просто одинична, я об'єднав її з матрицею View.

Перетворення нормальних векторів
Широко відомий такий факт:
Якщо у нас задана модель і вже пораховані (або, наприклад, задані руками) нормальні вектора до цієї моделі, і ця модель зазнає (аффинному) перетворення M, то нормальні вектора піддаються перетворенню, зворотному до транспонированному M.

Що-що?!

Цей момент залишається магічним для багатьох, але насправді, нічого чарівного тут немає. Розглянемо трикутник і вектор a, що є нормальним до його похилій грані. Якщо ми просто розтягнемо наш простір в два рази по вертикалі, то перетворений вектор a перестане бути нормальним до перетвореної межі.

Щоб прибрати весь наліт магії, потрібно зрозуміти одну просту річ: нам потрібно не просто перетворити нормальні вектора, нам потрібно порахувати нормальні вектора до перетвореної моделі.

Отже, у нас є вектор нормалі a=(A,B,C). Ми знаємо, що площина, що проходить через початок координат, має нормаллю вектор a (на нашій ілюстрації це похиле ребро лівого трикутника), задається рівнянням Ax+By+Cz=0. Давайте запишемо це рівняння в матричному вигляді, причому відразу в однорідних координатах:

Нагадую, що (A,B,C) — це вектор, тому отримує нуль в останню компоненту при зануренні в чотиривимірний простір, а (x,y,z) — це точка, тому до нього приписуємо 1.

Давайте додамо одиничну матрицю (М, помножена на зворотну до неї) в середину цього запису:


Вираз в правих дужках — це перетворені точки. У лівих — нормальний вектор! Так як в стандартній конвенції при лінійному відображенні ми записуємо вектори (точки) у стовпець (сподіваюся, ми не будемо розпалювати холивара про ко — і контравариантные вектора),
то попередній вираз може бути записано наступним чином:


Що приводить нас до вышеозначенному фактом, що нормаль до перетвореному об'єкту виходить перетворенням вихідної величини, оберненим до транспонированному M.

Зауважте, якщо M — це композиція паралельних переносів, обертань і однорідних растягиваний, то транспонована М дорівнює зворотному М, і вони один одного анулюють. Але так як наші матриці перетворень будуть включати в себе перспективне спотворення, це нам мало допоможе.

У поточному коді ми перетворення нормалей не використовуємо, але от у наступній статті про шейдери це буде дуже важливо.

Щасливого програмування!

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

0 коментарів

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