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

Зміст основного курсу


Поліпшення коду


Спілкування поза хабра
Якщо у вас є питання, і ви не хочете задавати їх у коментарях, або просто не маєте можливості писати в коментарі, приєднуйтесь до jabber-конференції xmpp: 3d@conference.sudouser.ru

Новий растеризатор і корекція перспективних спотворень
Тема сьогоднішнього розмови — це корекція спотворень інтерполяції, подивіться на різницю текстурування на підлозі:

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

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



Знаходження барицентрических координат точки в двовимірному трикутнику
Дан 2D трикутник ABC, точка P, все в картезианских координатах. Наше завдання знайти барицентричні координати точки P відносно трикутника ABC. Це трійка чисел (1-u-v, u, v), з допомогою яких ми можемо знайти точку P:

Це означає, що якщо ми помістимо ваги (1-u-v, u, v) у відповідні вершини трикутника, то центр мас системи опиниться в точці P. Рівно це ж саме можна переписати, сказавши, що точка P буде мати координати (u,v) в репере (A, AB, AC):


Отже, дано вектори AB, AC, AP, нам потрібно знайти два дійсних u,v, які відповідають наступним рівнянням:


Це векторне рівняння, яке еквівалентне системі двох звичайних рівнянь.


Система двох лінійних рівнянь з двома невідомими, все це справа дуже легко вирішується. Я лінивий, чесно виводити рішення не хочу, давайте вирішимо наступним чином. Перепишемо нашу систему в матричному вигляді:


Це означає, що ми шукаємо вектор (u,v,1), який одночасно ортогонален двом даним векторам (ABx,ACx,PAx) і (ABy,ACy,PAy). Вже зрозуміли, до чого я хилю? Правильно, ми просто векторно перемножимо (ABx,ACx,PAx) x (ABy,ACy,PAy) і поділимо на отриману третю компоненту.

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

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

Прихований текст
#include < vector>
#include < iostream>
#include "geometry.h"
#include "tgaimage.h"

const int width = 200;
const int height = 200;

Vec3f barycentric(Vec2i *pts, Vec2i P) {
Vec3f u = cross(Vec3f(pts[2][0]-pts[0][0], pts[1][0]-pts[0][0], pts[0][0]-P[0]), Vec3f(pts[2][1]-pts[0][1], pts[1][1]-pts[0][1], pts[0][1]-P[1]));
if (std::abs(u[2])<1) return Vec3f(-1,1,1); // triangle is degenerate, in this case return smth with negative coordinates
return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z);
}

void triangle(Vec2i *pts, TGAImage &image, TGAColor color) {
Vec2i bboxmin(image.get_width()-1, image.get_height()-1);
Vec2i bboxmax(0, 0);
Vec2i clamp(image.get_width()-1, image.get_height()-1);
for (int i=0; i < 3; i++) {
for (int j=0; j<2; j++) {
bboxmin[j] = std::max(0, std::min(bboxmin[j], pts[i][j]));
bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts[i][j]));
}
}
Vec2i P;
for (P. x=bboxmin.x; P. x<=bboxmax.x; P. x++) {
for (P. y=bboxmin.y; P. y<=bboxmax.y; P. y++) {
Vec3f bc_screen = barycentric(pts, P);
if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue;
image.set(P. x, P. y, color);
}
}
}

int main(int argc, char** argv) {
TGAImage frame(200, 200, TGAImage::RGB);
Vec2i pts[3] = {Vec2i(10,10), Vec2i(100, 30), Vec2i(190, 160)};
triangle(pts, frame, TGAColor(255, 0, 0));
frame.flip_vertically(); // to place the origin in the bottom left corner of the image
frame.write_tga_file("framebuffer.tga");
return 0;
}



Функція barycentric вважає координати точки P у даному трикутнику, ми тільки що обговорили в попередньому параграфі. Давайте розберемося, як працює функція triangle. Насамперед вона вважає описує прямокутник. Він заданий двома кутами — нижнім лівим та верхнім правим. Ми проходимо по всіх точках трикутника і знаходимо найменші і найбільші координати. На додаток до того я ще знайшов перетин описує прямокутника з рамкою екрана, щоб не витрачати час даремно, якщо наш рисуемый трикутник виходить за межі екрану.

Вітаю вас, ми навчилися малювати трикутник.


Корекція перспективного відображення

Як нам використовувати цей растеризатор в рендері? Здавалося б, заміни сходинку image.set(P. x, P. y, color) на виклик фрагментного шейдера, і справа з кінцем. На жаль, це не зовсім так.
Ось код,, який так і робить. Результат його роботи на головній картинці зліва. А ось його виправлення, яке нам дасть правильну рендер. Зміна строго одне: я передав в шейдер не барицентричні координати bc_screen, а барицентричні координати bc_clip. Уф. Давайте розбиратися.

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

Давайте поставимо задачу наступним чином. Ми знаємо, що якась точка P належить трикутника ABC, після перспективного поділу перетворюється в точку P' по наступному закону:

Ми знаємо барицентричні координати точки P' відносно трикутника a'b'c' (це перетворені вершини трикутника ABC):


Так от, знаючи координати трикутника a'b'c' і барицентричні координати точки P' щодо нього, нам потрібно знайти барицентричні координати точки P відносно трикутника ABC:


Отже, запишемо координати точки P':


Помножимо все на rP.z+1:


Отримали вираз P = [ABC]*[незрозумілий вектор]. Але ж це і є визначення барицентрических координат! Залишилася сама малість. Що нам відомо і що нам невідомо у визначенні цього вектора? Альфа-бета-гамма-все-штрих нам відомі. rA.z+1, rB.z+1, rC.z+1 нам відомі, це координати трикутника, передані в растеризатор. Залишилася одна річ = rP.z+1. Тобто, координата z точки P. І з її допомогою ми визначаємо точку P. Не замкнутий це коло? На щастя, немає.

Давайте використаємо той факт, що в (нормалізованих) барицентрических координатах сума координат дає одиницю, тобто, alpha+beta+gamma = 1:


Тепер у нас все відомо, ми можемо перетворити барицентричні координати в екранному просторі в барицентричні координати в глобальному просторі. Перетворення координат нелінійно, але саме це дозволяє робити нормальну лінійну інтерполяцію. І саме тому, передавши під фрагментний шейдер скориговані координати, ми отримуємо разюче іншу картину на текстурі в шашечку.

Отже, щоб знайти, наприклад, текстурні координати, нам достатньо (скалярно) помножити (uv0 uv1 uv2) на (alpha beta, gamma). Або (z0 z1 z2) (alpha beta, gamma). Або (vn0 vn1 vn2) на (alpha beta, gamma). І взагалі все, що нам потрібно інтерполювати!

В якості (необов'язкового) бонусу до курсу нам залишилося розібратися з тим, як вважати дотичний простір, щоб використовувати tangent-space текстури нормалей, зробити світяться поверхні, подивитися на те, що таке ambient occlusion.

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

0 коментарів

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