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



Видалення невидимих поверхонь.
Знайомтесь, це мій друг z-buffer голови абстрактного африканця. Він нам допоможе прибрати візуальні артефакти відкидання задніх граней, які у нас залишалися в минулій статті.


До речі, не можу не згадати, що ця модель, яку я використовую в хвіст і в гриву, була люб'язно надана чудовим Vidar Rapp.
Ми її можемо використовувати виключно в рамках навчання рендерингу. Це дуже якісна модель, з якою я по-варварськи обійшовся, але я обіцяю повернути їй очі!



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




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


Ось так повинен виглядати рендер цієї сцени.


Синя грань — вона за червоною або перед? Ні те, ні те. Алгоритм художника тут ламається. Ну, тобто, можна синю грань розбити на дві, одна частина перед червоною, інша. А та, що перед червоною, ще на дві — перед зеленою і за зеленою… Думаю, досить ясно, що в сценах з мільйонами трикутників це швидко стає непростим завданням. Так, у неї є рішення, наприклад, користуватися двійковими разбиениями простору, заодно це допомагає і для сортування при зміні положення камери, але давайте не будемо ускладнювати собі життя!




Три виміри — це занадто. Y-buffer!
Давайте втратимо одне з вимірювань, розглянемо двовимірну сцену, отриману перетином нашої сцени і жовтої площині розрізу:

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


Знімок коду, як правило, на гітхабі. Оскільки у нас сцена двовимірна, то її дуже просто намалювати, це просто три виклику функції line(), яку ми запрограмували в перший раз.

{ // just dumping the 2d scene (yay we have enough dimensions!)
TGAImage scene(width, height, TGAImage::RGB);

// scene "2d mesh"
line(Vec2i(20, 34), Vec2i(744, 400), scene, red);
line(Vec2i(120, 434), Vec2i(444, 400), scene, green);
line(Vec2i(330, 463), Vec2i(594, 200), scene, blue);

// screen line
line(Vec2i(10, 10), Vec2i(790, 10), scene, white);

scene.flip_vertically(); // i want to have the origin at the left bottom corner of the image
scene.write_tga_file("scene.tga");
}

Ось так виглядає наша двовимірна сцена, наше завдання подивитися на ці відрізки зверху.


Давайте тепер її рендери. Нагадую, рендер — це картинка шириною на всю сцену і висотою в один піксель. В моєму коді я її оголосив висотою в 16, але це щоб не ламати очі, розглядаючи один піксель на екрані високого дозволу. Функція rasterize пише тільки у першу сходинку картинки render.

TGAImage render(width, 16, TGAImage::RGB);
int ybuffer[width];
for (int i=0; i<width; i++) {
ybuffer[i] = std::numeric_limits<int>::min();
}
rasterize(Vec2i(20, 34), Vec2i(744, 400), render, red, ybuffer);
rasterize(Vec2i(120, 434), Vec2i(444, 400), render, green, ybuffer);
rasterize(Vec2i(330, 463), Vec2i(594, 200), render, blue, ybuffer);

Отже, я оголосив загадковий масив ybuffer рівно розмір нашого екрану (width, 1). Цей масив ініціалізований мінус нескінченності. Потім я передаю в функцію rasterize і картинку render, і цей загадковий масив. Як виглядає сама функція?

void rasterize(Vec2i p0, Vec2i p1, TGAImage &image, TGAColor color, int ybuffer[]) {
if (p0.x>p1.x) {
std::swap(p0, p1);
}
for (int x=p0.x; x<=p1.x; x++) {
float t = (x-p0.x)/(float)(p1.x-p0.x);
int y = p0.y*(1.-t) + p1.y*t;
if (ybuffer[x]<y) {
ybuffer[x] = y;
image.set(x, 0, color);
}
}
}

Дуже-дуже просто: я проходжу по всіх x-координатами між p0.x і p1.x і вираховую відповідну y-координату нашої лінії.
Потім я перевіряю, що у нас в масиві ybuffer по цій координаті x. Якщо поточний піксель ближче до камери, ніж те, що там зберігається,
я його малюю в картинці, і ставлю нову y-координату в ігрек-буфері.

Давайте розбиратися поетапно: після виклику растеризатора для першої (червоної) лінії ось що ми маємо в пам'яті:

вміст екрану:


вміст y-буфера:

Тут мерзенним фіолетовим кольором позначена мінус нескінченність, це ті місця, де ні одного пікселя ще намальовано не було.
Решта градацій сірого, т. к. ybuffer це не колір, а глибина даного пікселя. Чим біліше, тим ближче до камери був даний намальований на екрані піксель.

Далі ми малюємо зелену лінію, пам'ять після виклику її растеризатора:
вміст екрану:


вміст y-буфера:


Ну і наостанок синю:
вміст екрану:


вміст y-буфера:


Вітаю вас, ми намалювали нашу двовимірну сцену! Ще раз помилуємося на фінальний рендер:





Три виміри — це в самий раз. Z-buffer!
Знімок коду на гітхабі.

Увага: у цій статті я використовую ту ж саму версію растеризатора трикутника, що і в попередній. Поліпшена версія растеризатора (прохід пікселів описує прямокутника) буде незабаром люб'язно надана і описана в окремій статті шановним gbg! Stay tuned.



Оскільки у нас екран тепер двовимірний, то z-буфер теж повинен бути двовимірним:

int *zbuffer = new int[width*height];

Я спакував двовимірний масив одновимірний, конвертувати можна як зазвичай:
з двох координат в одну:

int idx = x + y*width;

Назад:

int x = idx % width;
int y = idx / width;

Потім в коді я проходжу по всіх трикутниках і роблю виклик растеризатора, передаючи йому і картинку, і z-буфер.

triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(intensity*255, intensity*255, intensity*255, 255), zbuffer);

[...]

void triangle(Vec3i t0, Vec3i t1, Vec3i t2, TGAImage &image, TGAColor color, int *zbuffer) {
if (t0.y==t1.y && t0.y==t2.y) return; // i dont care about degenerate triangles
if (t0.y>t1.y) std::swap(t0, t1);
if (t0.y>t2.y) std::swap(t0, t2);
if (t1.y>t2.y) std::swap(t1, t2);
int total_height = t2.y-t0.y;
for (int i=0; i<total_height; i++) {
bool second_half = i>t1.y-t0.y || t1.y==t0.y;
int segment_height = second_half? t2.y-t1.y: t1.y-t0.y;
float alpha = (float)i/total_height;
float beta = (float)(i-(second_half? t1.y-t0.y: 0))/segment_height; // be careful: with above conditions no division by zero here
Vec3i A = t0 + (t2-t0)*alpha;
Vec3i B = second_half? t1 + (t2-t1)*beta: t0 + (t1-t0)*beta;
if (A. x>B. x) std::swap(A, B);
for (int j=A. x; j<=B. x; j++) {
float phi = B. x==A. x? 1.: (float)(j-A. x)/(float)(B. x-A x);
Vec3i P = A + (B-A)*phi;
P. x = j; P. y = t0.y+i; // hack a to fill holes (due to int cast precision problems)
int idx = j+(t0.y+i)*width;
if (zbuffer[idx]<P. z) {
zbuffer[idx] = P. z;
image.set(P. x, P. y, color); // attention, due to int casts t0.y+i != A. y
}
}
}
}

Це просто жахливо, наскільки код схожий на растеризатор з минулої статті. Що змінилося? (Використовуйте vimdiff і подивіться :) )
Vec2 був замінений на Vec3 у виклику функції і зроблена перевірка if (zbuffer[idx]<P. z);
Всі! Ось наш справжній рендер без огріхів відсікання невидимих поверхонь:



Зверніть увагу, що backface culling в моєму коді залишений:

if (intensity>0) {
triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(intensity*255, intensity*255, intensity*255, 255), zbuffer);
}

Він не є необхідним для отримання цієї картинки, це тільки прискорення обчислень.




Стоп, ми тільки що інтерполювали z-координату. А можна додати ще чого навантаження?
Текстури! Це буде домашня робота.

В .obj файл є рядки vt u v, вони задають масив текстурних координат.
Середнє число між слешами в f x/x/x/x/x/x/x — це текстурні координати даної вершини в даному трикутнику. Интерполируете їх всередині трикутника, множите на ширину-висоту текстурного файлу і отримуєте колір пікселя з файлу текстури.
Дифузну текстуру брати тут.

Ось приклад того, що повинно вийти. Обертати голову не обов'язково :)

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

0 коментарів

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