在巴比伦JS和GLSL中实施SSAO,使用视图光线进行深度比较

时间:2017-09-06 15:25:10

标签: glsl depth depth-buffer babylonjs ssao

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

它是使用Babylon JS引擎创建的BABYLON.ShaderMaterial并设置在BABYLON.RenderTargetTexture中,主要受到这个着名的SSAO教程的启发:http://john-chapman-graphics.blogspot.fr/2013/01/ssao-tutorial.html

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

在解释整个事情之前,请注意 Babylon JS使用左手坐标系,这可能会对我的代码产生很大影响。

以下是我的经典步骤:

  1. 首先,我在我的JS代码中计算我的四个相机远平面角位置。它们可能每次都是常量,因为它们是在视图空间位置计算的。
  2. // 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.xposition.y标志让着色器知道每次传球时使用的角落。

    3. 然后在我的片段着色器中插入这些角以计算视线,即从摄像机到远平面的矢量(因此,其.z分量等于到摄像机的远平面距离)。

    4. 片段着色器遵循John Chapman教程的说明(请参阅下面的注释代码)。

    5. 我使用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()函数允许我在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
      

      结果如下:

      Please click to see my result

      结果边缘有瑕疵,近处阴影不是很强烈......我的代码中有人会看到错误或奇怪的东西吗?

      非常感谢!

1 个答案:

答案 0 :(得分:3)

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

samplePosition = samplePosition * radius + fragPositionVS;

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

在第occlusion += (samplePosition.z - sampleDepth >= depthBias ? 1.0 : 0.0) * rangeCheck;行中,您可以计算samplePosition.zsampleDepth的差异。虽然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个符号的限制,答案仅限于此):

JSFiddleGitHub

SSAOtest


扩展答案

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

(见OpenGL ES write depth data to color

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

此值已在片段着色器中计算,并包含在gl_FragCoord.z中。请参阅gl_FragCoord的Khronos Group参考页面,其中包含:

  

如果没有着色器包含对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);
}

另见以下问题的答案: