屏幕空间环境光遮蔽:ssaowebgl实现

本文详细介绍了如何在WebGL环境中实现屏幕空间环境光遮蔽(SSAO)技术,包括基本原理、计算准备、遮蔽率计算、采样点算法和frameShader算法等步骤。通过生成随机采样点并结合深度、法线纹理信息,计算得到遮蔽率,进而影响像素的颜色,从而实现SSAO效果。同时,文中提供了关键代码片段以帮助理解实现过程。
摘要由CSDN通过智能技术生成

目录

基本原理

计算SSAO准备工作

SSAO实现

如何计算遮蔽率

采样点算法

frameShader算法

最终效果


基本原理

计算一张全屏幕的AO纹理,在最终渲染渲染画面的frameShader中采样纹理得到一个遮蔽率,对fragment颜色值乘以该遮蔽率(遮蔽率越接近0,颜色更黑,遮蔽率越接近1,颜色则贴近原来的色)。

计算SSAO准备工作

在webgl中计算SSAO,相较于opengl有很多限制,我们在真正进入ssao核心算法计算前需要准备:

1.diffuseTexture:没有SSAO前直接渲染到screen的Texture需要保持;

2.depthTexture:webgl的depth_texture扩展开启可生成,本人在实现过程中就直接使用了扩展生成,当然你可以自己算;

3.normalTexture:记录模型的法线信息的Texture;

4.noiseTexture:可以在SSAO实现中客户端生成也可以预制一致noiseTexture;作用:采样核心随机转动,得到更丰富的随机值;

SSAO实现

如何计算遮蔽率

对逐个fragment周围的n个采样点做遮蔽测试,然后统计有百分之多少的采样点通过了测试,那么就得到了粗略的遮蔽率。

球型采样:即使一个平面没有被周围的平面遮蔽,该平面的遮蔽率也只是0.5。这样就会导致画面变灰。

半球采样,即限制采样点都在平面法向量同一侧。

采样点算法

这里我直接在CPU计算利用Math.random生成随机采样

    /*采样次数越多,遮蔽率就算得越准确,但性能也就下降。
      为了降低采样次数,为此要引入一个random noise随机化的旋转噪声贴图,
      使得相邻的fragment采样点差异性变大。*/Ï
    let kernelSize = this.kernelSize;//采样次数
    let kernel = this.kernel;//传入GPU的kernel数组
    for (let i = 0; i < kernelSize; i++) {
      let sample = new Vector3();//three.js的Vector3数据结构,下面有用到单位化函数与乘scale
      sample.x = Math.random() * 2 - 1;
      sample.y = Math.random() * 2 - 1;
      sample.z = Math.random();

      sample.normalize();

     // sample *= Math.random();// 单位化后随机分配距离 
      let scale = i / kernelSize;// 缩放因子,初始化为i/kernelSize是为了确保每一个点不会位置重复
      scale = _Math.lerp(0.1, 1, scale * scale);// 使得大部分采样点会更靠近原点
      sample.multiplyScalar(scale);// 应用缩放因子

      kernel.push(sample);
    }

frameShader算法

从depthTexture深度信息:传入当前的vUv

float getDepth(const in vec2 screenPosition) {
    
    return texture2D(tDepth, screenPosition).x;
    
}

依据当前的透视camera的到viewZ:传入上面得出的depth

float getViewZ(const in float depth) {
    
    
    return perspectiveDepthToViewZ(depth, cameraNear, cameraFar);//uniform传入的near与far
  
    
}
float perspectiveDepthToViewZ(const in float invClipZ, const in float near, const in float far) {
    return (near * far) / ((far - near) * invClipZ - far);
}

依据前两步计算出ViewPosition:传入vUv,depth,viewZ;

vec3 getViewPosition(const in vec2 screenPosition, const in float depth, const in float viewZ) {
    
    float clipW = cameraProjectionMatrix[2][3] * viewZ + cameraProjectionMatrix[3][3];
    
    vec4 clipPosition = vec4((vec3(screenPosition, depth) - 0.5) * 2.0, 1.0);
    
    clipPosition *= clipW; // unprojection.
    
    return (cameraInverseProjectionMatrix * clipPosition).xyz;//投影的矩阵都是CPU计算好uniform传入GPU的
    
}

得到viewNormal:传入vUv;

vec3 unpackRGBToNormal(const in vec3 rgb) {
    return 2.0 * rgb.xyz - 1.0;
}
vec3 getViewNormal(const in vec2 screenPosition) {
    
    return unpackRGBToNormal(texture2D(tNormal, screenPosition).xyz);
    
}

获取随机旋转向量与和TBN矩阵:

tangent向量的计算。需要构造出的TBN的z方向是normal的方向,所以未知数就是相应的x、y方向,而因为正交矩阵的一个基可以用另外2个基做叉乘得到,所以未知的y方向(bitangent)等于normal和tangent的cross。真正要算的只有x的方向:tangent向量。tangent向量,必然和normal正交,但方向和randomVec有关(所以randomVec才被称为旋转向量)。

 vec2 noiseScale = vec2(resolution.x / 4.0, resolution.y / 4.0);//resolution为当前宽高
    vec3 random = normalize(texture2D(tNoise, vUv * noiseScale).xyz); // 获取随机旋转向量并单位化

    // TBN左乘samplePos就可以把samplePos从tangent space转换到view space
    vec3 tangent = normalize(random - viewNormal * dot(random, viewNormal)); //x
    vec3 bitangent = normalize(cross(viewNormal, tangent)); //y,也可不用单位化
    mat3 kernelMatrix = mat3(tangent, bitangent, viewNormal); //z

最终计算:

float getLinearDepth(const in vec2 screenPosition) {
    
    
    float fragCoordZ = texture2D(tDepth, screenPosition).x;
    float viewZ = perspectiveDepthToViewZ(fragCoordZ, cameraNear, cameraFar);
    return viewZToOrthographicDepth(viewZ, cameraNear, cameraFar);
    
}
 float viewZToOrthographicDepth(const in float viewZ, const in float near, const in float far) {
    return (viewZ + near) / (near - far);
}
///

for(int i = 0; i < KERNEL_SIZE; i ++ ) {//传入采样数组长度
        
        vec3 sampleVector = kernelMatrix * kernel[i]; // reorient sample vector in view space // 切线->观察空间
        vec3 samplePoint = viewPosition + (sampleVector * kernelRadius); // calculate sample point
        
        vec4 samplePointNDC = cameraProjectionMatrix * vec4(samplePoint, 1.0); // project point and calculate NDC// 观察->裁剪空间
        samplePointNDC /= samplePointNDC.w; // 透视划分,得到NDC坐标
        
        
        vec2 samplePointUv = samplePointNDC.xy * 0.5 + 0.5; // compute uv coordinates// 变换到0.0 - 1.0的值域
        
        float realDepth = getLinearDepth(samplePointUv); // get linear depth from depth texture//view space
        float sampleDepth = viewZToOrthographicDepth(samplePoint.z, cameraNear, cameraFar); // compute linear depth of the sample view Z value
        float delta = sampleDepth - realDepth;
//这里与opengl略有不同
        if (delta > minDistance && delta < maxDistance) { // if fragment is before sample point, increase occlusion//自己在外部设置或者frame里预设minDis与maxDis
            
            occlusion += 0.6;
            
        }
        
    }
    
    occlusion = clamp(occlusion / float(KERNEL_SIZE), 0.0, 1.0);
    
    gl_FragColor = vec4(vec3(1.0 - occlusion), 1.0);

最终效果

参考文献:

https://learnopengl.com/Advanced-Lighting/SSAO

https://github.com/McNopper/OpenGL/blob/master/Example28/shader/ssao.frag.glsl

http://john-chapman-graphics.blogspot.com/2013/01/ssao-tutorial.html

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值