gpt4 book ai didi

glsl - Babylon JS 和 GLSL 中的 SSAO 实现,使用 View 光线进行深度比较

转载 作者:行者123 更新时间:2023-12-01 13:30:39 27 4
gpt4 key购买 nike

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

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

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

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

这是我的经典步骤:

  1. 首先,我在我的 JS 代码中计算了我的四个相机远平面角的位置。它们可能每次都是常数,因为它们是在 View 空间位置中计算的。
// 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 教程的说明(请参阅下面的注释代码)。

我使用 DepthRenderer.getDepthMap() 方法将深度缓冲区作为 BABYLON.RenderTargetTexture 获取。深度纹理查找实际上返回(根据 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 位 float 获取当前 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

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

非常感谢!

最佳答案

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

samplePosition = samplePosition * radius + fragPositionVS;

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

occlusion += (samplePosition.z - sampleDepth >= depthBias ? 1.0 : 0.0) * rangeCheck; 行中,您计算​​ samplePosition.zsampleDepth 的差值。 samplePosition.z-near-far 之间的 View 空间坐标,而 sampleDepth 是 [0, 1] 范围内的深度。计算这两个值之间的差异也没有任何意义。

如果您想计算距离或比较距离,我建议始终使用 Z 坐标。

如果您有深度值,则可以通过将深度值转换为规范化设备坐标并将规范化设备坐标转换为 View 坐标来计算 View 空间中的 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] 范围内的值,映射从近平面的距离和到远平面的距离(在 View 空间中)的范围,但不是线性的(对于透视投影)。< br/>因此,代码行 vec3 fragPositionVS = (vCornerPositionVS / far) * fragDepth; 不会计算出正确的片段位置,但您可以这样做:

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

请注意,在 View 空间中,z 轴从视口(viewport)出来。如果角位置设置在 View 空间中,则 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);

在顶点着色器中,角位置的分配是混合的。视口(viewport)的左下角位置为 (-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轴在视口(viewport)的中心),这可以简化:

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 中。请参阅 Khronos Group 的 gl_FragCoord 引用页,上面写着:

The z component is the depth value that would be used for the fragment's depth if no shader contained any writes to 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);
}

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

关于glsl - Babylon JS 和 GLSL 中的 SSAO 实现,使用 View 光线进行深度比较,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46079061/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com