Краткий курс компьютерной графики: пишем упрощённый OpenGL своими руками, статья 4б из 6
Сегодня мы заканчиваем с ликбезом по геометрии, в следующий раз будет веселье с шейдерами! Чтобы не было совсем скучно, вот вам тонировка Гуро: Я убрал текстуры, чтобы было виднее. Тонировка Гуро очень проста: добрый дяденька-моделёр дал нам нормальные вектора к каждой вершине объекта, они хранятся в строчках 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: подставим во вторую часть выражение замены базиса:
И это нам даст формулу замены координат для двух базисов.
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).
Если вы помните, то у меня в коде встречались подобные конструкции: 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 — это композиция параллельных переносов, вращений и однородных растягиваний, то транспонированная М равняется обратной М, и они друг друга аннулируют. Но так как наши матрицы преобразований будут включать в себя перспективное искажение, это нам мало поможет.
В текущем коде мы преобразование нормалей не используем, но вот в следующей статье про шейдеры это будет очень важно.
Счастливого программирования!