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

в 11:49, , рубрики: game development, геометрия для пятого класса, Программирование, Работа с анимацией и 3D-графикой

Содержание основного курса

Улучшение кода

Общение вне хабра

Если у вас есть вопросы, и вы не хотите задавать их в комментариях, или просто не имеете возможности писать в комментарии, присоединяйтесь к jabber-конференции xmpp: 3d@conference.sudouser.ru

Новый растеризатор и коррекция перспективных искажений

Тема сегодняшего разговора — это коррекция искажений интерполяции, посмотрите на разницу текстурирования на полу:
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 1
Я специально убрал из рендера всё, что касается освещения, нормалей и прочего, оставил только текстуру. Спасибо MrShoor, я был ленив и не делал этой коррекции, но в итоге заморочился, благодаря его пинку. Со старой версией растеризатора это было муторно, с новой это достаточно просто.

Поэтому начнём с того, как работает новый растеризатор, а для этого нам нужно уметь работать с барицентрическими координатами.

Нахождение барицентрических координат точки в двумерном треугольнике

Дан 2D треугольник ABC, точка P, всё в картезианских координатах. Наша задача найти барицентрические координаты точки P относительно треугольника ABC. Это тройка чисел (1-u-v, u, v), с помощью которых мы можем найти точку P:
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 2
Это означает, что если мы поместим веса (1-u-v, u, v) в соответствующие вершины треугольника, то центр масс системы окажется в точке P. Ровно это же самое можно переписать, сказав, что точка P будет иметь координаты (u,v) в репере (A, AB, AC):
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 3

Итак, даны векторы AB, AC, AP, нам нужно найти два вещественных u,v, которые отвечают следующему уравнению:
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 4

Это векторное уравнение, которое эквивалентно системе двух обычных уравнений.
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 5

Система двух линейных уравнений с двумя неизвестными, всё это дело весьма нетрудно решается. Я ленивый, честно выводить решение не хочу, давайте решим следующим образом. Перепишем нашу систему в матричном виде:
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 6

Это означает, что мы ищем вектор (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. Перво-наперво она считает описывающий прямоугольник. Он задан двумя его углами — нижним левым и верхним правым. Мы проходим по всем точкам треугольника и находим наименьшие и наибольшие координаты. В добавление к тому я ещё нашёл пересечение описывающего прямоугольника с прямоугольником экрана, чтобы не тратить время попусту, если наш рисуемый треугольник выходит за пределы экрана.

Поздравляю вас, мы научились рисовать треугольник.
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 7

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

Как нам использовать этот растеризатор в рендере? Казалось бы, замени строчку image.set(P.x, P.y, color) на вызов фрагментного шейдера, и дело с концом. К сожалению, это не совсем так.
Вот код, который так и делает. Результат его работы на заглавной картинке слева. А вот его исправление, которое нам даст правильный рендер. Изменение строго одно: я передал в шейдер не барицентрические координаты bc_screen, а барицентрические координаты bc_clip. Уф. Давайте разбираться.

Проблема в том, что при перспективе мы в какой-то момент делили на последнюю однородную координату, сломав линейность нашего пайплайна. Поэтому мы не имеем права пользоваться барицентрическими координатами нашего пикселя для интерполяции атрибутов в оригинальном пространстве (будь то текстурные координаты или просто глубина).

Давайте поставим задачу следующим образом. Мы знаем, что некая точка P, принадлежащая треугольнику ABC, после перспективного деления превращается в точку P' по следующему закону:
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 8
Мы знаем барицентрические координаты точки P' относительно треугольника A'B'C' (это преобразованные вершины треугольника ABC):
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 9

Так вот, зная координаты треугольника A'B'C' и барицентрические координаты точки P' относительно него, нам нужно найти барицентрические координаты точки P относительно треугольника ABC:
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 10

Итак, запишем координаты точки P':
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 11

Умножим всё на rP.z+1:
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 12

Получили выражение P = [ABC]*[непонятный вектор]. Но ведь это и есть определение барицентрических координат! Осталась самая малость. Что нам известно и что нам неизвестно в определении этого вектора? Альфа-бета-гамма-всё-штрих нам известны. rA.z+1, rB.z+1, rC.z+1 нам известны, это координаты треугольника, переданные в растеризатор. Осталась одна вещь = rP.z+1. То есть, координата z точки P. И с её помощью мы определяем точку P. Не замкнутый ли это круг? К счастью, нет.

Давайте используем тот факт, что в (нормализованных) барицентрических координатах сумма координат даёт единицу, то есть, alpha+beta+gamma = 1:
Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4в из 6 - 13

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

Итак, чтобы найти, например, текстурные координаты, нам достаточно (скалярно) умножить (uv0 uv1 uv2) на (alpha beta gamma). Или (z0 z1 z2) на (alpha beta gamma). Или (vn0 vn1 vn2) на (alpha beta gamma). И вообще всё, что нам нужно интерполировать!

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

Автор: haqreu

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js