Реализация SSAO в Babylon JS и GLSL с использованием луча просмотра для сравнения глубины

Я пытаюсь создать свой собственный шейдер SSAO при прямом рендеринге (не при постобработке) с помощью GLSL. Я сталкиваюсь с некоторыми проблемами, но я действительно не могу понять, что не так с моим кодом.

Он создан с помощью движка Babylon JS как BABYLON.ShaderMaterial и установлен как BABYLON.RenderTargetTexture, и в основном он основан на этом известном руководстве по SSAO: http://john-chapman-graphics.blogspot.fr/2013/01/ssao-tutorial.html

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

Прежде чем объяснять все это, обратите внимание, что Babylon JS использует левостороннюю систему координат, что может иметь место в моем коде.

Вот мои классические шаги:

  1. Во-первых, я вычисляю положения дальних углов четырех камер в своем JS-коде. Они могут быть постоянными каждый раз, поскольку они вычисляются в пространственном положении.
// Calculating 4 corners manually in view space
var tan = Math.tan;
var atan = Math.atan;
var ratio = SSAOSize.x / SSAOSize.y;
var far = scene.activeCamera.maxZ;
var fovy = scene.activeCamera.fov;
var fovx = 2 * atan(tan(fovy/2) * ratio);
var xFarPlane = far * tan(fovx/2);
var yFarPlane = far * tan(fovy/2);

var topLeft     = new BABYLON.Vector3(-xFarPlane,  yFarPlane, far);
var topRight    = new BABYLON.Vector3( xFarPlane,  yFarPlane, far);
var bottomRight = new BABYLON.Vector3( xFarPlane, -yFarPlane, far);
var bottomLeft  = new BABYLON.Vector3(-xFarPlane, -yFarPlane, far);

var farCornersVec = [topLeft, topRight, bottomRight, bottomLeft];
var farCorners = [];

for (var i = 0; i < 4; i++) {
    var vecTemp = farCornersVec[i];
    farCorners.push(vecTemp.x, vecTemp.y, vecTemp.z);
}
  1. Эти угловые позиции отправляются в вершинный шейдер, поэтому векторные координаты сериализуются в массиве farCorners[] для отправки в вершинный шейдер.

  2. В моем вершинном шейдере знаки position.x и position.y сообщают шейдеру, какой угол использовать при каждом проходе.

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

  4. Фрагментный шейдер следует инструкциям руководства Джона Чепмена (см. код с комментариями ниже).

Я получаю свой буфер глубины как BABYLON.RenderTargetTexture с помощью метода DepthRenderer.getDepthMap(). Поиск текстуры глубины фактически возвращает (согласно шейдерам глубины Babylon JS): (gl_FragCoord.z / gl_FragCoord.w) / far, с:

  • gl_FragCoord.z: нелинейная глубина
  • gl_FragCoord.z = 1/Wc, где Wc — позиция вершины в пространстве отсечения (т.е. gl_Position.w в вершинном шейдере)
  • far: положительное расстояние от камеры до дальней плоскости.

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

Поскольку у меня нет нормальной текстуры, я вычисляю их из текущего значения буфера глубины с помощью getNormalFromDepthValue():

vec3 getNormalFromDepthValue(float depth) {
    vec2 offsetX = vec2(texelSize.x, 0.0);
    vec2 offsetY = vec2(0.0, texelSize.y);
    // texelSize = size of a texel = (1/SSAOSize.x, 1/SSAOSize.y)

    float depthOffsetX = getDepth(depthTexture, vUV + offsetX); // Horizontal neighbour
    float depthOffsetY = getDepth(depthTexture, vUV + offsetY); // Vertical neighbour

    vec3 pX = vec3(offsetX, depthOffsetX - depth);
    vec3 pY = vec3(offsetY, depthOffsetY - depth);
    vec3 normal = cross(pY, pX);
    normal.z = -normal.z; // We want normal.z positive

    return normalize(normal); // [-1,1]
}

Наконец, моя функция getDepth() позволяет мне получить значение глубины при текущем UV в 32-битном формате с плавающей запятой:

float getDepth(sampler2D tex, vec2 texcoord) {
    return unpack(texture2D(tex, texcoord));
    // unpack() retreives the depth value from the 4 components of the vector given by texture2D()
}

Вот мои коды вершинных и фрагментных шейдеров (без объявлений функций):

// ---------------------------- Vertex Shader ----------------------------
precision highp float;

uniform float fov;
uniform float far;
uniform vec3 farCorners[4];

attribute vec3 position; // 3D position of each vertex (4) of the quad in object space
attribute vec2 uv; // UV of each vertex (4) of the quad

varying vec3 vPosition;
varying vec2 vUV;
varying vec3 vCornerPositionVS;

void main(void) {
    vPosition = position;
    vUV = uv;

    // Map current vertex with associated frustum corner position in view space:
    // 0: top left, 1: top right, 2: bottom right, 3: bottom left
    // This frustum corner position will be interpolated so that the pixel shader always has a ray from camera->far-clip plane.
    vCornerPositionVS = vec3(0.0);

    if (positionVS.x > 0.0) {
        if (positionVS.y <= 0.0) { // top left
        vCornerPositionVS = farCorners[0];
        }
        else if (positionVS.y > 0.0) { // top right
            vCornerPositionVS = farCorners[1];
        }
    }
    else if (positionVS.x <= 0.0) {
        if (positionVS.y > 0.0) { // bottom right
            vCornerPositionVS = farCorners[2];
        }
        else if (positionVS.y <= 0.0) { // bottom left
            vCornerPositionVS = farCorners[3];
        }
    }

    gl_Position = vec4(position * 2.0, 1.0); // 2D position of each vertex
}
// ---------------------------- Fragment Shader ----------------------------
precision highp float;    

uniform mat4 projection; // Projection matrix
uniform float radius; // Scaling factor for sample position, by default = 1.7
uniform float depthBias; // 1e-5
uniform vec2 noiseScale; // (SSAOSize.x / noiseSize, SSAOSize.y / noiseSize), with noiseSize = 4

varying vec3 vCornerPositionVS; // vCornerPositionVS is the interpolated position calculated from the 4 far corners

void main() {
    // Get linear depth in [0,1] with texture2D(depthBufferTexture, vUV)
    float fragDepth = getDepth(depthBufferTexture, vUV);
    float occlusion = 0.0;

    if (fragDepth < 1.0) {
        // Retrieve fragment's view space normal
        vec3 normal = getNormalFromDepthValue(fragDepth); // in [-1,1]

        // Random rotation: rvec.xyz are the components of the generated random vector
        vec3 rvec = texture2D(randomSampler, vUV * noiseScale).rgb * 2.0 - 1.0; // [-1,1]
        rvec.z = 0.0; // Random rotation around Z axis

        // Get view ray, from camera to far plane, scaled by 1/far so that viewRayVS.z == 1.0
        vec3 viewRayVS = vCornerPositionVS / far;

        // Current fragment's view space position
        vec3 fragPositionVS = viewRay * fragDepth;

        // Creation of TBN matrix
        vec3 tangent = normalize(rvec - normal * dot(rvec, normal));
        vec3 bitangent = cross(normal, tangent);
        mat3 tbn = mat3(tangent, bitangent, normal);

        for (int i = 0; i < NB_SAMPLES; i++) {
            // Get sample kernel position, from tangent space to view space
            vec3 samplePosition = tbn * kernelSamples[i];

           // Add VS kernel offset sample to fragment's VS position
            samplePosition = samplePosition * radius + fragPosition;

            // Project sample position from view space to screen space:
            vec4 offset = vec4(samplePosition, 1.0);
            offset = projection * offset; // To view space
            offset.xy /= offset.w; // Perspective division
            offset.xy = offset.xy * 0.5 + 0.5; // [-1,1] -> [0,1]

            // Get current sample depth:
            float sampleDepth = getDepth(depthTexture, offset.xy);

            float rangeCheck = abs(fragDepth - sampleDepth) < radius ? 1.0 : 0.0;
            // Reminder: fragDepth == fragPosition.z

            // Range check and accumulate if fragment contributes to occlusion:
            occlusion += (samplePosition.z - sampleDepth >= depthBias ? 1.0 : 0.0) * rangeCheck;
        }
    }

    // Inversion
    float ambientOcclusion = 1.0 - (occlusion / float(NB_SAMPLES));
    ambientOcclusion = pow(ambientOcclusion, power);
    gl_FragColor = vec4(vec3(ambientOcclusion), 1.0);
}

Размытие по Гауссу по горизонтали и вертикали впоследствии очищает шум, создаваемый случайной текстурой.

Мои параметры:

NB_SAMPLES = 16
radius = 1.7
depthBias = 1e-5
power = 1.0

Вот результат:

Нажмите, чтобы увидеть мой результат

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

Большое спасибо!


person Damian Taylor    schedule 06.09.2017    source источник


Ответы (1)


fragPositionVS — это позиция в пространственных координатах вида, а radius — длина в координатах вида. Вы используете их для вычисления samplePosition:

samplePosition = samplePosition * radius + fragPositionVS;

Но в строке rangeCheck = abs(fragDepth - sampleDepth) < radius ? 1.0 : 0.0; вы сравниваете разницу fragDepth и sampleDepth с radius. Это не имеет смысла, поскольку fragDepth и sampleDepth — это значения из буфера глубины, диапазон [0, 1] и радиус — это длина в пространстве просмотра.

В строке occlusion += (samplePosition.z - sampleDepth >= depthBias ? 1.0 : 0.0) * rangeCheck; вы вычисляете разницу между samplePosition.z и sampleDepth. В то время как samplePosition.z — это координата пространства обзора между -near и -far, sampleDepth — это глубина в диапазоне [0, 1]. Вычисление разницы между этими двумя значениями также не имеет никакого смысла.

Я предлагаю всегда использовать координаты Z, если вы хотите рассчитать расстояния или сравнить расстояния.

Если у вас есть значение глубины, координата Z в пространстве обзора может быть рассчитана путем преобразования значения глубины в нормализованную координату устройства и преобразования нормализованной координаты устройства в координату вида:

float DepthToZ( in float depth )
{
    float near  = .... ; // distance to near plane (absolute value)
    float far   = .... ; // distance to far plane (absolute value)
    float z_ndc = 2.0 * depth - 1.0;
    float z_eye = 2.0 * near * far / (far + near - z_ndc * (far - near));
    return -z_eye;
}

Глубина представляет собой значение в диапазоне [0, 1] и отображает диапазон от расстояния до ближней плоскости и расстояния до дальней плоскости (в пространстве обзора), но не линейно (для перспективной проекции).
По этой причине строка кода vec3 fragPositionVS = (vCornerPositionVS / far) * fragDepth; не будет вычислять правильную позицию фрагмента, но вы можете сделать это следующим образом:

vec3 fragPositionVS = vCornerPositionVS * abs( DepthToZ(fragDepth) / far );

Обратите внимание, что в пространстве просмотра ось z выходит из окна просмотра. Если угловые позиции установлены в пространстве обзора, то координата Z должна быть отрицательным расстоянием до дальней плоскости:

var topLeft     = new BABYLON.Vector3(-xFarPlane,  yFarPlane, -far);
var topRight    = new BABYLON.Vector3( xFarPlane,  yFarPlane, -far);
var bottomRight = new BABYLON.Vector3( xFarPlane, -yFarPlane, -far);
var bottomLeft  = new BABYLON.Vector3(-xFarPlane, -yFarPlane, -far);

В вершинном шейдере назначение угловых позиций смешанное. Нижнее левое положение области просмотра равно (-1,-1), а верхнее правое положение — (1,1) (в нормированных координатах устройства).
Адаптируйте код следующим образом:

JavaScript:

var farCornersVec = [bottomLeft, bottomRight, topLeft, topRight];

Вершинный шейдер:

// bottomLeft=0*2+0*1, bottomRight=0*2+1*1, topLeft=1*2+0*1, topRight=1*2+1*1;
int i = (positionVS.y > 0.0 ? 2 : 0) + (positionVS.x > 0.0 ? 1 : 0);
vCornerPositionVS = farCorners[i];

Обратите внимание: если бы вы могли добавить дополнительный атрибут вершины для положения угла, это было бы проще.

Расчет положения фрагмента можно упростить, если известны соотношение сторон, угол поля зрения и нормированные аппаратные координаты фрагмента (положение фрагмента в диапазоне [-1,1]):

ndc_xy   = vUV * 2.0 - 1.0;
tanFov_2 = tan( radians( fov / 2 ) )
aspect   = vp_size_x / vp_size_y
fragZ    = DepthToZ( fragDepth );
fragPos  = vec3( ndc_xy.x * aspect * tanFov_2, ndc_xy.y * tanFov_2, -1.0 ) * abs( fragZ );

Если известна матрица перспективной проекции, ее можно легко рассчитать:

vec2 ndc_xy       = vUV.xy * 2.0 - 1.0;
vec4 viewH        = inverse( projection ) * vec4( ndc_xy, fragDepth * 2.0 - 1.0, 1.0 );
vec3 fragPosition = viewH.xyz / viewH.w;

Если перспективная проекция симметрична (поле зрения не смещено и ось Z пространства вида находится в центре окна просмотра), это можно упростить:

vec2 ndc_xy       = vUV.xy * 2.0 - 1.0;
vec3 fragPosition = vec3( ndc_xy.x / projection[0][0], ndc_xy.y / projection[1][1], -1.0 ) * abs(DepthToZ(fragDepth));

Смотрите также:


Я предлагаю написать фрагментный шейдер как-то так:

float fragDepth = getDepth(depthBufferTexture, vUV);
float ambientOcclusion = 1.0;
if (fragDepth > 0.0)
{
    vec3 normal = getNormalFromDepthValue(fragDepth); // in [-1,1]
    vec3 rvec = texture2D(randomSampler, vUV * noiseScale).rgb * 2.0 - 1.0;
    rvec.z = 0.0;
    vec3 tangent = normalize(rvec - normal * dot(rvec, normal));
    mat3 tbn = mat3(tangent, cross(normal, tangent), normal);

    vec2 ndc_xy = vUV.xy * 2.0 - 1.0;
    vec3 fragPositionVS = vec3( ndc_xy.x / projection[0][0], ndc_xy.y / projection[1][1], -1.0 ) * abs( DepthToZ(fragDepth) );
    // vec3 fragPositionVS = vCornerPositionVS * abs( DepthToZ(fragDepth) / far );

    float occlusion = 0.0;
    for (int i = 0; i < NB_SAMPLES; i++)
    {
        vec3 samplePosition = fragPositionVS + radius * tbn * kernelSamples[i];

        // Project sample position from view space to screen space:
        vec4 offset  = projection * vec4(samplePosition, 1.0);
        offset.xy   /= offset.w;               // Perspective division -> [-1,1]
        offset.xy    = offset.xy * 0.5 + 0.5;  // [-1,1] -> [0,1]

        // Get current sample depth
        float sampleZ = DepthToZ( getDepth(depthTexture, offset.xy) );

        // Range check and accumulate if fragment contributes to occlusion:
        float rangeCheck = step( abs(fragPositionVS.z - sampleZ), radius );
        occlusion += step( samplePosition.z - sampleZ, -depthBias ) * rangeCheck;
    }
    // Inversion
    ambientOcclusion = 1.0 - (occlusion / float(NB_SAMPLES));
    ambientOcclusion = pow(ambientOcclusion, power);
}
gl_FragColor = vec4(vec3(ambientOcclusion), 1.0);


См. пример WebGL, демонстрирующий полный алгоритм (к сожалению, полный код превысит ограничение в 30000 знаков, которым ограничен ответ):

JSFiddle или GitHub

SSAOtest


Расширение к ответу

Глубина в том виде, в каком она хранится в буфере глубины, рассчитывается следующим образом:

(см. OpenGL ES записывает данные глубины в цвет)

float ndc_depth = vPosPrj.z / vPosPrj.w;
float depth     = ndc_depth * 0.5 + 0.5;

Это значение уже вычислено во фрагментном шейдере и содержится в gl_FragCoord.z. См. справочную страницу Khronos Group для gl_FragCoord, где говорится :

Компонент z — это значение глубины, которое будет использоваться для глубины фрагмента, если ни один шейдер не содержит записи в gl_FragDepth.

Если глубина должна быть сохранена в буфере RGBA8, глубина должна быть закодирована в 4 байта буфера, чтобы избежать потери точности, и должна быть декодирована при чтении из буфера:

кодировать

vec3 PackDepth( in float depth )
{
    float depthVal = depth * (256.0*256.0*256.0 - 1.0) / (256.0*256.0*256.0);
    vec4 encode = fract( depthVal * vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    return encode.xyz - encode.yzw / 256.0 + 1.0/512.0;
}

декодировать

float UnpackDepth( in vec3 pack )
{
  float depth = dot( pack, 1.0 / vec3(1.0, 256.0, 256.0*256.0) );
  return depth * (256.0*256.0*256.0) / (256.0*256.0*256.0 - 1.0);
}

Смотрите также ответы на следующие вопросы:

person Rabbid76    schedule 06.09.2017
comment
На самом деле это значение глубины, отображаемое в моем пиксельном шейдере глубины, так что его gl_FragColor = vec4(depth). - person Damian Taylor; 08.09.2017
comment
Интересный материал и очень понятные объяснения! После некоторых исследований я понял, что могу столкнуться с проблемами отображения: мой объект виден только при чрезмерном увеличении (находясь за пределами усеченной пирамиды обзора). Любопытно то, что я вижу это, когда глубина вычисляется с помощью матрицы проекции, но не когда она рассчитывается с интерполированными углами усеченной пирамиды... Я пока продолжу исследовать эту часть. - person Damian Taylor; 08.09.2017
comment
Я так не думаю, Babylon JS имеет левостороннюю систему координат, что делает положительную ось Z направленной к экрану в пространстве обзора! - person Damian Taylor; 12.09.2017
comment
подождите, а дальние углы усеченного конуса отправляются из вершинного шейдера во фрагментный шейдер через vUV??? - person Damian Taylor; 13.09.2017
comment
1) Я не понимаю. Мои дальние углы усеченного конуса были сгенерированы до того, как шейдер пройдет в моем коде .js и передаст их вершинным шейдерам в массиве farCorners размера 4. Каждый угол передавался во фрагментный шейдер через vCornerPositions. Однако они не использовались, так как вы рекомендовали мне использовать это: vec3 fragPositionVS = vec3( ndc_xy.x * projection[0][0], ndc_xy.y * projection[1][1], -1.0 ) * abs( DepthToZ(fragDepth) ); вместо этого: vec3 fragPositionVS = vCornerPositionVS * abs( DepthToZ(fragDepth) / far ); 2) Это * projection[0][0] или / projection[0][0] ?? - person Damian Taylor; 14.09.2017
comment
Просто чтобы получить полное представление об этом, не могли бы вы указать, что именно здесь представляет vUV? Для меня это просто набор из двух координат в [0,1], что еще? Большое спасибо! - person Damian Taylor; 14.09.2017
comment
vec2 vUV находится в диапазоне [0, 1]. (0, 0) — нижний левый угол, а (1, 1) — верхний правый. ndc_xy = vUV.xy * 2.0 - 1.0; находится в диапазоне [-1, 1] (нормализованные координаты устройства). vec4 viewH = inverse( projection ) * vec4( ndc.xy, depth*2.0-1.0, 1.0 ); vec3 view = viewH.xyz / viewH.w даст позицию просмотра. vec3 view = vec3( ndc_xy.x / projection[0][0], ndc_xy.y / projection[1][1], -1.0 ) * abs( DepthToZ(fragDepth) ); делает то же самое без дорогого inverse( projection ). - person Rabbid76; 14.09.2017
comment
Я добавил некоторые детали в свой код, чтобы он стал понятнее. Мне интересно, правильно ли работает моя функция getNormalFromDepth()... там могут быть проблемы с нелинейностью. Левосторонняя система координат также может включать в себя отрицательные знаки в некоторых частях, это становится для меня очень грязным... Кстати, что такое fragZ в вычислении положения фрагмента? - person Damian Taylor; 15.09.2017
comment
@DamianTaylor fragZ = DepthToZ( fragDepth ); - getNormalFromDepth() не должно быть проблемой, но я проверю - person Rabbid76; 15.09.2017
comment
@DamianTaylor Отображение угловых точек в вершинном шейдере было смешанным - см. редактирование ответа. И, может быть, вы должны попробовать normal.z = abs( normal.z ); вместо normal.z = -normal.z; - person Rabbid76; 15.09.2017