Получение исходных 3D-координат по значениям из z-буфера.

За счет использования значений из буфера глубины (z-буфера) можно для каждого пиксела изображения восстановить исходные трехмерные координаты в пространстве наблюдателя (камеры).

Однако для этого сначала надо вспомнить как именно происходит преобразование координат в OpenGL. Для начала координаты задаются в системе координат объекта (object space). Умножение на модельно-видовую матрицу переводит координаты в систему координат наблюдателя/камеры (eye space). Далее, умножение на матрицу проектирования переводит координаты в систему координат отсечения (clip space). Перспективное деление (деление на w) переводит координаты в нормализованные координаты устройства (NDC).

Все три компоненты нормализованных координат устройства лежат в диапазоне [-1,1]. Для записи в буфер глубины zNDC переводится из [-1,1] в [0,1] при помощи линейного преобразования (точно такого, какое переводит нормали в цвета для карт нормалей).

Рассмотрим теперь как можно восстановить координаты в системе координат наблюдателя, используя значение в буфере глубины.

Одним из способов достижения этого является приведение позиции фрагмента к кубу [-1,1]3 и последующем умножении на обращенную матрицу проектирования gl_ProjectionMatrixInverse. Ниже приводится реализующий это фрагментный шейдер (спасибо MrShoor) указавшему на этот способ.

uniform sampler2D depthMap;
uniform vec2      windowSize;   // viewport's width and height
uniform vec2      texSize;      // width and height of filled part of NPOT texture

void main()
{
    vec2 texCoord = gl_FragCoord.xy / texSize; // remap fragment coords to texture coords (for NPOT texture)
    float depth   = texture2D ( depthMap, texCoord).r;

                                // remap xy into [-1,1]^3 and multiply by matrix
    vec4  pixelPos   = gl_ProjectionMatrixInverse * vec4((gl_FragCoord.xy/windowSize * 2.0 - 1.0), depth * 2.0 - 1.0, 1.0);     
    
    pixelPos /= pixelPos.w;     // set w to 1
}

Однако в ряде случаев можно избежать умножения на матрицы 4x4 (не говоря уже про ее обращение) и получить результат за меньшее число операций. Нам понадобится понимание того, как именно происходит вычисление значения, записываемого в буфер глубины.

Первым нашим шагом будет восстановление по значению из буфера глубины (d) значения zeye - z-координаты в системе координат наблюдателя.

Для того, чтобы понять как это возможно, необходимо разобраться в работе стандартного перспективного проектирования в OpenGL. Оно производится при помощи следующей матрицы проектирования (задаваемой, например, командой gluPerspective):

Здесь использованы следующие обозначения:

Достаточно рассмотреть преобразования только двух последних координат - z и w при помощи соответствующей части матрицы проектирования.

После перспективного деления мы получаем значение zNDC:

Однако следует иметь в виду, что стандартное перспективное преобразование в OpenGL использует в качестве направления взгляда отрицательное направление оси Oz и переводит усеченную пирамиду видимости в куб [-1,1]3. Поэтому полученное по предыдущей формуле значение zNDC будет пробегать отрезок [-1,1] при zeye пробегающем значения из отрезка [-zFar,zNear].

Соответственно значение d, действительно записываемое в буфер глубины, получается по следующей формуле:

Обратив эту формулу относительно zeye получим следующую формулу:

Обратите внимание, что здесь через d обозначено значение глубины, прочитанное из буфера глубины и принимающее значение из отрезка [0,1]. Подобную формулу можно найти, например здесь. Однако иногда встречаются и другие варианты этой же формулы.

Во-первых это связано с тем, какой знак z мы хотим получить - в OpenGL направление взгляда идет в сторону, противоположную оси Oz, что не всегда удобно. Поэтому иногда встречается вариант этой формулы, отличающийся знаком.

Другим вариантом этой же формулы является случай, когда мы используем не само значение, прочитанное из буфера глубины, а zNDC, т.е. значение из отрезка [-1,1]. Для этого в исходную формулу просто подставляется преобразование, переводящее отрезок [-1,1] в [0,1] и в результате мы получаем следующую формулу (ее можно найти, например, здесь):

Таким образом, скопировав после первого прохода (а сейчас довольно часто рендеринг начинается с depth-only-прохода) буфер глубины в текстуру, на последующих проходах читая во фрагментном шейдере из этой текстуры значения глубины d можно легко восстановить соответствующие значения zeye.

Осталось только восстановить две оставшиеся пространственные координаты xeye и yeye. Самым простым способом достижения этого является вывод прямоугольника с zeye=1 границы которого соответствуют пересечению данной плоскости и (неусеченной) пирамиды видимости.

Переданные координаты вершин передаются через varying vec3-переменную во фрагментный шейдер и их достаточно просто умножить на вычисленное значение zeye для получения полного набора пространственных координат (xeye,yeye,zeye).

//
// Example of fragment shader 
//

varying vec3      pos;
uniform sampler2D depthMap;
uniform float     zNear, zFar;

void main()
{
    float d        = texture2D ( depthMap, gl_TexCoord [0].st ).a;
    float zEye     = zFar * zNear / ( d * (zFar - zNear) - zFar );
    vec3  eyeCoord = pos * zEye;
}

Ниже приводится соответствующий вершинный шейдер.

varying vec3 pos;

void main(void)
{
    pos         = vec3 ( gl_ModelViewMatrix * gl_Vertex );          // transformed point to world space
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}