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

Давайте знайомитися, це я.

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

Минулого разу ми намалювали дротяну сітку тривимірної моделі, в цей раз ми заллємо полігони. Точніше, трикутники, так як OpenGL практично будь полігон триангулирует, тому ні до чого розбирати складний випадок. Нагадую, що цей цикл статей створений для самостійного програмування. Час, який я тут наводжу — це не час читання мого коду. Це час написання коду з нуля. Мій код тут тільки для того, щоб порівняти ваш (робочий) код з моїм. Я зовсім не є хорошим програмістом, тому ваш код може бути істотно краще мого. Будь-яка критика вітається, будь питань радий.

будь Ласка, якщо ви прямуєте цього туториалу і пишете свій код, викладайте його на github.com/code.google.com і їм подібні і давайте посилання в коментарях! Це може добре допомогти і вам (інші люди можуть чогось порадити), так і майбутнім читачам.


Малюємо заповнений трикутник.
Отже, тема на сьогодні (приблизно на дві години для погано програмуючих, але мотивованих студентів): побудова двовимірних трикутників. Минулого разу ми розібрали алгоритм Брезенхэма для растеризації відрізка, тепер завдання намалювати заповнений трикутник. Ви будете сміятися, але це нетривіальне завдання. Я не знаю чому, але я знаю, що це так. Більшість моїх студентів без підказок проводять над цим завданням істотно більше пари годин. Давайте визначимося з методом, а потім будемо програмувати.

На самому початку давайте розглянемо ось такий псевдокод:

triangle(vec2 points[3]) {
vec2 bbox[2] = find_bounding_box(points);
for (each pixel in the bounding box) {
if (inside(bbox, pixel)) {
put_pixel(pixel);
}
}
}

Я дуже люблю цей метод. Він простий і робочий. Знайти описує прямокутник вкрай просто, перевірити належність точки двовимірному трикутника (та й будь-якому опуклому полігону) теж просто.

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

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

Окей, початкова заглушка буде виглядати наступним чином:

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
line(t0, t1, image, color);
line(t1, t2, image, color);
line(t2, t0, image, color);
}

[...]

Vec2i t0[3] = {Vec2i(10, 70), Vec2i(50, 160), Vec2i(70, 80)};
Vec2i t1[3] = {Vec2i(180, 50), Vec2i(150, 1), Vec2i(70, 180)};
Vec2i t2[3] = {Vec2i(180, 150), Vec2i(120, 160), Vec2i(130, 180)};

triangle(t0[0], t0[1], t0[2], image, red);
triangle(t1[0], t1[1], t1[2], image, white);
triangle(t2[0], t2[1], t2[2], image, green);



Як зазвичай, на гітхабі доступний відбиток коду. У цьому коді все просто: я даю три трикутника для початкової налагодження вашого коду; якщо всередині функції triangle просто зробити виклик line(), то отримаємо контур трикутника. Як намалювати заповнений трикутник?

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

Традиційно використовується line sweeping (замітання відрізком?):
  • Сортуємо вершини трикутника за їх y-координати
  • Растеризуем паралельно ліву і праву границі трикутника
  • Промальовуємо горзонтальный відрізок між лівою і правою точкою кордону


Тут мої студенти починають губитися, хто лівий, хто правий, та і взагалі, в трикутнику три відрізка…
В цей момент я залишаю своїх студентів приблизно на годину, читання мого коду куди як менш цінне, ніж порівняння свого вистражданого!) коду з моїм.

[пройшов годину]

Як я малюю? Ще раз, якщо у вас є кращий метод, то я його з величезним задоволенням візьму на озброєння. Давайте припустимо, що у нас є три точки трикутника, t0,t1,t2, вони відсортовані по зростанню y-координати.
Тоді границя А буде між t0 і t2, межа Б буде між t0 і t1, а потім між t1 і t2.

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
// sort the vertices, t0, t1, t2 lower-to-upper (bubblesort yay!)
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);

line(t0, t1, image, green);
line(t1, t2, image, green);
line(t2, t0, image, red);
}

Тут у нас кордон А намальований червоним, а межа Б зеленим.



Межа Б, на жаль, складова. Давайте отрисуем нижню половину трикутника, розрізавши його по горизонталі в точці зламу кордону Б.

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
// sort the vertices, t0, t1, t2 lower-to-upper (bubblesort yay!)
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 y=t0.y; y<=t1.y; y++) {
int segment_height = t1.y-t0.y+1;
float alpha = (float)(y-t0.y)/total_height;
float beta = (float)(y-t0.y)/segment_height; // be careful with divisions by zero
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = t0 + (t1-t0)*beta;
image.set(A. x, y, red);
image.set(B. x, y, green);
}
}



Зауважте, що в цей раз у мене вийшли розривні відрізки. На відміну від минулого разу (де ми малювали прямі) я не заморочился поворотом зображення на 90°. Чому? Це виявляється не всім очевидним моментом. Просто якщо ми з'єднаємо горизонтальними лініями відповідні пари точок, то прогалини пропадуть:



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

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
// sort the vertices, t0, t1, t2 lower-to-upper (bubblesort yay!)
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 y=t0.y; y<=t1.y; y++) {
int segment_height = t1.y-t0.y+1;
float alpha = (float)(y-t0.y)/total_height;
float beta = (float)(y-t0.y)/segment_height; // be careful with divisions by zero
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = t0 + (t1-t0)*beta;
if (A. x>B. x) std::swap(A, B);
for (int j=A. x; j<=B. x; j++) {
image.set(j, y, color); // attention, due to int casts t0.y+i != A. y
}
}
for (int y=t1.y; y<=t2.y; y++){
int segment_height = t2.y-t1.y+1;
float alpha = (float)(y-t0.y)/total_height;
float beta = (float)(y-t1.y)/segment_height; // be careful with divisions by zero
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = t1 + (t2-t1)*beta;
if (A. x>B. x) std::swap(A, B);
for (int j=A. x; j<=B. x; j++) {
image.set(j, y, color); // attention, due to int casts t0.y+i != A. y
}
}
}



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

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
if (t0.y==t1.y && t0.y==t2.y) return; // i dont care about degenerate triangles
// sort the vertices, t0, t1, t2 lower-to-upper (bubblesort yay!)
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
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i 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++) {
image.set(j, t0.y+i, color); // attention, due to int casts t0.y+i != A. y
}
}
}

Відбиток коду для відтворення 2d трикутників.

Малюємо модель
Ми вже вміємо малювати модель з порожніми трикутниками, давайте їх заллємо випадковим кольором, це допоможе нам перевірити, наскільки добре ми закодували заповнення трикутників. Ось код.

for (int i=0; i<model->nfaces(); i++) {
std::vector < int> face = model->face(i);
Vec2i screen_coords[3];
for (int j=0; j < 3; j++) {
Vec3f world_coords = model->vert(face[j]);
screen_coords[j] = Vec2i((world_coords.x+1.)*width/2., (world_coords.y+1.)*height/2.);
}
triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(rand()%255, rand()%255, rand()%255, 255));
}

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


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



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

for (int i=0; i<model->nfaces(); i++) {
std::vector < int> face = model->face(i);
Vec2i screen_coords[3];
Vec3f world_coords[3];
for (int j=0; j < 3; j++) {
Vec3f v = model->vert(face[j]);
screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);
world_coords[j] = v;
}
Vec3f n = (world_coords[2]-world_coords[0])^(world_coords[1]-world_coords[0]);
n.normalize();
float intensity = n*light_dir;
if (intensity>0) {
triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(intensity*255, intensity*255, intensity*255, 255));
}
}

Але ж скалярний твір може бути негативним, що це означає? Це означає, що світло падає позаду полігону. Якщо модель хороша (зазвичай не наша турбота, а 3д моделеров), то ми просто можемо цей трикутник не малювати. Це дозволяє швидко прибрати частину невидимих трикутників. В англомовній літературі називається Back-face culling.


Модель моєї голови виглядає детальніше? Ну так в ній чверть мільйона трикутників. Нічого, деталі ми додамо пізніше, отримавши картинку, яку я дав для затравки в першій статті.

Зверніть увагу, внутрішня порожнина рота намалювалася поверх губ. Ну а що, таке швидке відсікання невидимих трикутників прибирає все непотрібне тільки для опуклих моделей. Ці огріхи ми приберемо в наступний раз, закодований z-buffer.
Поточна версія рендерера.

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

0 коментарів

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