3

我正在尝试使用 GLSL 在前向渲染(而不是后处理)中创建自己的 SSAO 着色器。我遇到了一些问题,但我真的无法弄清楚我的代码有什么问题。

它是使用 Babylon JS 引擎作为 a 创建BABYLON.ShaderMaterial并设置在 aBABYLON.RenderTargetTexture中,它的主要灵感来自这个著名的 SSAO 教程:http: //john-chapman-graphics.blogspot.fr/2013/01/ssao-tutorial.html

出于性能原因,我必须在屏幕空间中不投影和不投影的情况下进行所有计算,我宁愿使用上面教程中描述的视图射线方法。

在解释整个事情之前,请注意巴比伦 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. 片段着色器遵循 John Chapman 教程的说明(参见下面的注释代码)。

BABYLON.RenderTargetTexture我使用该DepthRenderer.getDepthMap()方法获得了深度缓冲区。深度纹理查找实际上返回(根据 Babylon JS 的深度着色器):, (gl_FragCoord.z / gl_FragCoord.w) / far其中:

  • gl_FragCoord.z:非线性深度
  • gl_FragCoord.z = 1/WcWc剪辑空间顶点位置在哪里(即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()函数允许我以 32 位浮点数获取当前 UV 的深度值:

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

结果如下:

请点击查看我的结果

结果在其边缘有伪影,并且接近的阴影不是很强烈......有人会在我的代码中看到错误或奇怪的东西吗?

非常感谢!

4

1 回答 1

3

fragPositionVS是视图空间坐标中的位置,radius是视图坐标中的长度。您使用它们来计算samplePosition

samplePosition = samplePosition * radius + fragPositionVS;

但是在该行rangeCheck = abs(fragDepth - sampleDepth) < radius ? 1.0 : 0.0;中,您将 和 的差异fragDepth进行sampleDepth比较radius。这是没有意义的,因为fragDepthsampleDepth是深度缓冲区中的值,范围 [0, 1] 和半径是视图空间中的长度。

在该行occlusion += (samplePosition.z - sampleDepth >= depthBias ? 1.0 : 0.0) * rangeCheck;中,您计算​​ 和 的samplePosition.z差值sampleDepth。WhilesamplePosition.z是介于-near和之间的视图空间坐标-farsampleDepth是范围 [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 个符号的限制,答案仅限于此):

JSFiddleGitHub

SSAO测试


扩展答案

将存储在深度缓冲区中的深度计算如下:

(参见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);
}

另请参阅以下问题的答案:

于 2017-09-06T21:12:05.173 回答