GAMES202实时渲染(3)-Screen Space Ray Tracing

内容参考闫令琪课程《games202-高质量实时渲染及作业3》、“202作业3代码模板”花桑博客

实现效果

cube1_开光追_采样1次

cube2_开光追_采样1次

cube2_开光追_采样5次

光线追踪-洞穴-采样5

洞穴_开光追_采样20次

原理

空间中一个点接收到的光照有"直接光照"、“间接光照"两种类型,两种光照都包含的效果叫"全局光照”
全局光照

  • 直接光照,即光直接照在物体表面,比如太阳光,手电筒光

正方体只有直接光照,背光面都是黑的

  • 间接光照,物体自身不发光,将光照反射到其他物体表面
    直接光照 + 间接光照

屏幕空间光线追踪(screen space ray tracing),有两层含义:

  1. 光线追踪。通过追踪光线传播的路径对物体进行着色,和光栅化着色有明显的区别

ray tracing

  1. 屏幕空间。借助屏幕空间的数据,对光线的路径求交,起到加速的作用。这些数据已gBuffer的形式存储,主要用到深度缓冲(depth buffer)

光线追踪的核心计算就是求交,如何快速的找到光线在空间中击中的第一个点。

重点-屏幕空间求交

屏幕空间求交

  1. 根据法线得到光线反射的方向
  2. 进入循环操作,每次前进一小步后判断是否有交点
  3. 从深度图中,取p0(x, y)对应的深度值,即p1的z坐标,如果p0.z > p1.z,则说明光线和p0相交

这个算法只是近似的算法。如果求交的物体是往里边凹进去的,p0.z > p1.z 也不一定相交。但为了追求效率,暂且这么假设。

关键代码说明

绘制流程

除开灯,整个场景渲染经过3道pass

  1. 第一次pass生成阴影图(shadow map)
  2. 第二次pass生成集合缓冲(GBuffer)
  3. 第三次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使用参考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。可以继续对光追做优化,用自适应的方式调整求交的步幅,限于篇幅,暂不展开。

猜你喜欢

转载自blog.csdn.net/daozi20/article/details/129512178