Как именно OpenGL выполняет перспективную коррекцию линейной интерполяции?
Если на этапе растеризации в конвейере OpenGL происходит линейная интерполяция, а вершины уже преобразованы в пространство экрана, откуда берется информация о глубине, используемая для перспективно правильной интерполяции?
Кто-нибудь может дать подробное описание того, как OpenGL переходит от примитивов экранного пространства к фрагментам с правильно интерполированными значениями?
2 ответа
Вывод вершинного шейдера представляет собой четырехкомпонентный вектор, vec4 gl_Position
, Из раздела 13.6 Преобразования координат ядра GL 4.4 spec:
Координаты обрезки для вершины являются результатом выполнения шейдера, который дает координату вершины
gl_Position
,Разделение перспективы по координатам клипа дает нормализованные координаты устройства, после чего следует преобразование области просмотра (см. Раздел 13.6.1) для преобразования этих координат в координаты окна.
Рассмотрим типичную проекционную матрицу с левой, верхней, правой, нижней и ближней плоскостями отсечения ±1. Это выглядит как:
[ 1 0 0 0 ]
[ 0 1 0 0 ]
P = [ * * * * ]
[ 0 0 -1 0 ]
Мы игнорируем третью строку, потому что она используется только для вычисления значения глубины для z-буфера и не имеет отношения к остальной части обсуждения.
Для данной вершины (x, y, z, 1) в мировом пространстве вершинный шейдер передаст значение
gl_Position = P * vec4(x, y, z, 1) = vec4(x, y, *, -z)
на этапе растеризации. Вот откуда берется перспективная информация: она находится в последнем компоненте gl_Position
! Обратите внимание, что вершинный шейдер не несет ответственности за деление перспективы. Если вы это сделаете, вы получите неправильную интерполяцию.
Как вычисляется правильная перспективная интерполяция?
Рассмотрим плоскость, проходящую через данный треугольник. Мы даем ему параметризацию (s, t):
x = s*x0 + t*x1 + x2
y = s*y0 + t*y1 + y2
z = s*z0 + t*z1 + z2
Позволять u = -x/z
, v = -y/z
быть координатами устройства. Подставьте выражения для x, y, z и решите для (s,t). обозначать w = -1/z
и заменить z, s и t. Если никто из нас не ошибся, вы должны получить
(y0*z1 - z0*y1)*u + (z0*x1 - x0*z1)*v + (x0*y1 - y0*x1)
w = -1 * -------------------------------------------------------------- .
(y0*z1 - z0*y1)*x2 + (z0*x1 - x0*z1)*y2 + (x0*y1 - y0*x1)*z2
Мы получили, что обратная глубина (w) является аффинной относительно координат устройства (u,v). Таким образом, мы можем вычислить w в вершинах треугольника и интерполировать его линейно во внутренности.
Далее мы хотим интерполировать некоторый произвольный атрибут. Очевидно, достаточно рассчитать (s, t) параметризацию для каждого фрагмента.
Решая (x,y) для (s, t), получаем:
(x - x2)*y1 - x1*(y - y2)
s = ---------------------------
x0*y1 - x1*y0
x0*(y - y2) - (x - x2)*y0
t = ---------------------------
x0*y1 - x1*y0
где (x,y) = (u,v)/w
непосредственно из определений. Таким образом, мы выразили (s, t) как функцию (u,v), с одним делением на фрагмент и большим количеством muls и добавлений.
Обратите внимание, что это теоретическая часть. Реальные аппаратные реализации могут выполнять множество приближений, уловок и других действий.
Собираем все вместе
Для простоты предположим, что преобразование области просмотра является тождественным, так что координаты окна совпадают с нормализованными координатами устройства.
Рассмотрим растеризацию одного треугольника, для которого вершинный шейдер вернул позиции P0, P1, P2 (это их
gl_Position
s) и некоторые атрибуты A0, A1, A2, которые мы должны правильно интерполировать.обозначать
Qi = (Pi.x, Pi.y, -Pi.w)
,вычисление
(x0, y0, z0) = Q0 - Q2 (x1, y1, z1) = Q1 - Q2 (x2, y2, z2) = Q2 .
(Это делается один раз за треугольник.)
Для каждой вершины вычислить
Ui = Pi.x / Pi.w Vi = Pi.y / Pi.w
Это дает свою позицию на экране.
Растеризуйте 2D-треугольник с помощью сеток (U0, V0), (U1, V1), (U2, V2), создав набор положений фрагментов (u,v), которые лежат внутри треугольника.
Для каждого фрагмента (u,v), полученного вышеуказанной растеризацией, выполните следующее:
Замените (u, v) в формулах предыдущего раздела, указав (s, t) фрагмент.
- (s, t) линейно изменяются в мировом пространстве (с (0,0) в P2, (1,0) в P0 и (0,1) в P1) и в перспективе корректно в пространстве экрана.
подсчитывать
A = s*(A0 - A2) + t*(A1 - A2) + A2
Выполните фрагментный шейдер с входом A (который правильно интерполирован).
Формула, которую вы найдете в спецификации GL (см. Стр. 427; ссылка - текущая спецификация 4.4, но так было всегда) для интерполяции с коррекцией перспективы значения атрибута в треугольнике:
a * f_a / w_a + b * f_b / w_b + c * f_c / w_c
f=-----------------------------------------------------
a / w_a + b / w_b + c / w_c
где a,b,c
обозначим барицентрические координаты точки в треугольнике, для которого мы интерполируем (a,b,c >=0, a+b+c = 1
), f_i
значение атрибута в вершине i
, а также w_i
клип пространство w
координата вершины i
, Обратите внимание, что барицентрические координаты рассчитываются только для 2D-проекции координат оконного пространства треугольника (поэтому z игнорируется).
Это то, что формулы, которые Ибунгалоубилл дал в своем прекрасном ответе, сводятся, в общем случае, к произвольной оси проекции. Фактически, последний ряд матрицы проекции определяет только ось проекции, к которой плоскость изображения будет ортогональна, и пространство клипа w
Компонент - это просто произведение точек между координатами вершины и этой осью.
В типичном случае матрица проекции имеет (0,0,-1,0) в качестве последней строки, поэтому она преобразуется так, что w_clip = -z_eye
и это то, что использовал ybungalowbill. Тем не менее, так как w
это то, что мы на самом деле будем делать деление (это единственный нелинейный шаг во всей цепочке преобразования), это будет работать для любой оси проекции. Это также будет работать в тривиальном случае ортогональных проекций, где w
всегда 1 (или, по крайней мере, постоянный).
Обратите внимание на несколько вещей для эффективной реализации этого. Инверсия
1/w_i
может быть предварительно рассчитан для каждой вершины (давайте назовем ихq_i
в последующем) его не нужно переоценивать для каждого фрагмента. И это абсолютно бесплатно, так как мы делим наw
во всяком случае, при входе в пространство NDC, поэтому мы можем сохранить это значение. Спецификация GL никогда не описывает, как определенная функция должна быть реализована внутри, но тот факт, что координаты экранного пространства будут доступны вglFragCoord.xyz
, а такжеgl_FragCoord.w
гарантированно дает (линеарий интерполированный)1/w
Координатапространства клипа довольно показательна. Это за фрагмент1_w
значение на самом деле является знаменателем формулы, приведенной выше.Факторы
a/w_a
,b/w_b
а такжеc/w_c
каждый используется два раза в формуле. И они также постоянны для любого значения атрибута, теперь независимо от того, сколько атрибутов нужно интерполировать. Таким образом, для каждого фрагмента вы можете рассчитатьa'=q_a * a
,b'=q_b * b
а такжеc'=q_c
и получитьa' * f_a + b' * f_b + c' * f_c f=------------------------------ a' + b' + c'
Таким образом, перспективная интерполяция сводится к
- 3 дополнительных умножения,
- 2 дополнительных дополнения и
- 1 дополнительное подразделение
за фрагмент.