目录
基本原理
计算一张全屏幕的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