habrahabr

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

  • воскресенье, 25 января 2015 г. в 02:11:07
http://habrahabr.ru/post/248723/

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



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



Я убрал текстуры, чтобы было виднее. Тонировка Гуро очень проста: добрый дяденька-моделёр дал нам нормальные вектора к каждой вершине объекта, они хранятся в строчках 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, x'y'z'). Но ведь модель задана в репере (O, xyz), значит, нам нужно посчитать репер x'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[i][3] = -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,d], где 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 — это композиция параллельных переносов, вращений и однородных растягиваний, то транспонированная М равняется обратной М, и они друг друга аннулируют. Но так как наши матрицы преобразований будут включать в себя перспективное искажение, это нам мало поможет.

В текущем коде мы преобразование нормалей не используем, но вот в следующей статье про шейдеры это будет очень важно.

Счастливого программирования!