内容参考闫令琪课程《games202-高质量实时渲染及作业3》、“202作业3代码模板”、花桑博客
实现效果
原理
空间中一个点接收到的光照有"直接光照"、“间接光照"两种类型,两种光照都包含的效果叫"全局光照”
- 直接光照,即光直接照在物体表面,比如太阳光,手电筒光
- 间接光照,物体自身不发光,将光照反射到其他物体表面
屏幕空间光线追踪(screen space ray tracing),有两层含义:
- 光线追踪。通过追踪光线传播的路径对物体进行着色,和光栅化着色有明显的区别
- 屏幕空间。借助屏幕空间的数据,对光线的路径求交,起到加速的作用。这些数据已gBuffer的形式存储,主要用到深度缓冲(depth buffer)
光线追踪的核心计算就是求交,如何快速的找到光线在空间中击中的第一个点。
重点-屏幕空间求交
- 根据法线得到光线反射的方向
- 进入循环操作,每次前进一小步后判断是否有交点
- 从深度图中,取p0(x, y)对应的深度值,即p1的z坐标,如果p0.z > p1.z,则说明光线和p0相交
这个算法只是近似的算法。如果求交的物体是往里边凹进去的,p0.z > p1.z 也不一定相交。但为了追求效率,暂且这么假设。
关键代码说明
绘制流程
除开灯,整个场景渲染经过3道pass
- 第一次pass生成阴影图(shadow map)
- 第二次pass生成集合缓冲(GBuffer)
- 第三次pass绘制到默认frame buffer(屏幕)
WebGLRenderer.js
// Draw light
light.meshRender.mesh.transform.translate = light.entity.lightPos;
light.meshRender.draw(this.camera, null, updatedParamters);
// Shadow pass
gl.bindFramebuffer(gl.FRAMEBUFFER, light.entity.fbo);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
for (let i = 0; i < this.shadowMeshes.length; i++) {
this.shadowMeshes[i].draw(this.camera, light.entity.fbo, updatedParamters);
}
// Buffer pass
gl.bindFramebuffer(gl.FRAMEBUFFER, this.camera.fbo);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
for (let i = 0; i < this.bufferMeshes.length; i++) {
this.bufferMeshes[i].draw(this.camera, this.camera.fbo, updatedParamters);
}
// Camera pass
for (let i = 0; i < this.meshes.length; i++) {
this.meshes[i].draw(this.camera, null, updatedParamters);
}
GBuffer里包含5个纹理buffer
FBO.js
//创建帧缓冲区对象
var framebuffer = gl.createFramebuffer();
if(!framebuffer){
console.log("无法创建帧缓冲区对象");
return error();
}
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
var GBufferNum = 5;
framebuffer.attachments = [];
framebuffer.textures = []
for (var i = 0; i < GBufferNum; i++) {
var attachment = gl_draw_buffers['COLOR_ATTACHMENT' + i + '_WEBGL'];
var texture = CreateAndBindColorTargetTexture(framebuffer, attachment);
framebuffer.attachments.push(attachment);
framebuffer.textures.push(texture);
}
// * Tell the WEBGL_draw_buffers extension which FBO attachments are
// being used. (This extension allows for multiple render targets.)
gl_draw_buffers.drawBuffersWEBGL(framebuffer.attachments);
WEBGL_draw_buffers是webgl 2.0的API,大部分浏览器都支持
WEBGL_draw_buffers使用参考webgl_draw_buffers
间接光照
伪代码:
基于蒙特卡洛积分实现
- 随机取一个方向,求交点p1
- 计算交点p1对当前点p0的光照加成,累加到总和中
- 求平均(蒙特卡洛积分)
逻辑在片元着色器中,ssrFragment.glsl
#define SAMPLE_NUM 1
void main() {
float s = InitRand(gl_FragCoord.xy);
vec3 L = vec3(0.0);
// L = GetGBufferDiffuse(GetScreenCoordinate(vPosWorld.xyz));
vec3 worldPos = vPosWorld.xyz;
vec2 screenUV = GetScreenCoordinate(vPosWorld.xyz);
vec3 wi = normalize(uLightDir);
vec3 wo = normalize(uCameraPos - worldPos);
// 直接光照
L = EvalDiffuse(wi, wo, screenUV) * EvalDirectionalLight(screenUV);
vec3 L_ind = vec3(0.0);
for(int i = 0; i < SAMPLE_NUM; i++) {
float pdf;
vec3 localDir = SampleHemisphereCos(s, pdf);
vec3 normal = GetGBufferNormalWorld(screenUV);
vec3 b1, b2;
LocalBasis(normal, b1, b2);
vec3 dir = normalize(mat3(b1, b2, normal) * localDir);
vec3 position_1;
if(RayMarch(worldPos, dir, position_1)){
vec2 hitScreenUV = GetScreenCoordinate(position_1);
L_ind += EvalDiffuse(dir, wo, screenUV) / pdf * EvalDiffuse(wi, dir, hitScreenUV) * EvalDirectionalLight(hitScreenUV);
}
}
L_ind /= float(SAMPLE_NUM);
L = L + L_ind;
vec3 color = pow(clamp(L, vec3(0.0), vec3(1.0)), vec3(1.0 / 2.2));
gl_FragColor = vec4(vec3(color.rgb), 1.0);
}
从逻辑上看是从着色点向外采样,找到次级光源的影响。但是便于理解,我们可以从直接光源反向推导
补充
开了光追性能较差,我的台式机是3090的显卡,一两万的显卡采样20次帧率不到6fps。可以继续对光追做优化,用自适应的方式调整求交的步幅,限于篇幅,暂不展开。