作业要求
本轮作业中,我们需要在一个光源为方向光,材质为漫反射 (Diffuse) 的场景中,完成屏幕空间下的全局光照效果(两次反射)。为了在作业框架中实现上述效果,基于我们需要的信息不同我们会三阶段着色,每个阶段都有相对应的任务。第一次着色负责计算 ShadowMap 所需的深度值并保存到贴图中。第二次着色负责计算屏幕空间中,每个像素应的世界坐标系下位置、世界坐标系下法线、漫反射反射率和可见性信息并最保存到对应贴图中。第三次着色基于之前得到的场景几何信息 (像素对应的置,法线),场景与光源的遮挡信息 (光源坐标系的深度值),场景的材质信息 (漫反反射率),来计算两次反射的全局光照结果。本轮作业的主要工作将集中在第三次着色中。注意:一般情况下,我们应该在第二次着色时计算直接光照信息并存下来,在之后计算间接光照时进行查询。不过,为了实现方便我们将这部工作也转移到了第三次着色中完成。
根据上面的作业总览,我们得知在该作业框架的渲染流程包含3次Pass
第一次是Shadow Pass,先绘制场景的ShadowMap
第二次是GBuffer Pass,记录的是摄像机视角的可视信息。根据要渲染的点,该Pass结合ShadowMap和模型的diffuse map、normal map生成对应片元的diffuse、depth、normal、shadow、worldPos五个GBuffer信息
第三次是Camera Pass,渲染的是最终的显示内容,包含了直接光和简介光照的计算
作业框架已经帮我们完成了第一个和第二个Pass,我们只需要完善第三个Pass的内容。
在第三Pass内,我们需要补全Light直接光的部分和Light_ind间接光的部分
直接光照
要计算直接光,我们先得知道,在作业框架中,光照用的是兰伯特(Lambertian)光照模型,兰伯特光照模型公式的伪代码如下
Color=Kd*I*dot(N,L)
其步骤十分简单,转化到该作业框架中就是,我们需要获取该点的Kd也就是漫反射贴图中的漫反射系数,I是光的强度,N是该点的法向量,L是光源方向。
根据框架内容,我们可以用EvalDiffuse和EvalDirectionalLight这两个函数计算光照。EvalDiffuse负责计算Kd*dot(N,L)项,EvalDirectionalLight负责计算I项,另外用于计算阴影的View也需要写进EvalDirectionalLight内
//ssrFragment.glsl
vec3 EvalDiffuse(vec3 wi, vec3 wo, vec2 uv) {
//根据uv的值我们可以通过框架中的函数获取一些列数据
//从Diffuse Map获取反射率
vec3 albedo=GetGBufferDiffuse(uv);
vec3 normal=GetGBufferNormalWorld(uv);
float Cos=max(0.0,dot(normal,wi));
//这里需要除以一个π来维持能量守恒
return albedo*Cos*INV_PI;
}
vec3 EvalDirectionalLight(vec2 uv) {
//注释中说明了需要把阴影的计算写进这函数里
vec3 Le = GetGBufferuShadow(uv)*uLightRadiance;
return Le;
}
在上面EvalDiffuse函数中,我们在最后乘上了INV_PI,这是为了保持能量守恒,INV_PI是π的倒数。
为了搞明白为什么要除以π,我们从入射光和出射光的能量出发。假设Ei是入射光能量、Eo是出射光能量。且一定满足Eo<=Ei
则Eo满足Eo=所有方向的出射光的能量的总和。
这里需要乘一个cosθ是因为Lo隐含了单位方向角,我们需要投影到表面上来衡量表面上的功率。
因为在漫反射表面,我们可以认为同一个点的颜色在任何方向的观察结果都一样,所以可以认为Lo是常数,我们可以提出Lo
我们可以对Cosθ进行半球积分,详细过程如下,我们可以得到积分的结果是π,得出Eo=Lo*π
我们假设能量是守恒的,则
Ei=Eo=Lo*π
我们可以算出Lo=Ei/π,也就是总入射能量除以π,所以我们可以近似的理解为,Lo=Li/π,所以上面公EvalDiffuse最后需要除以π。
准确的原因参考这篇文章:为什么BRDF的漫反射项要除以π?
SSR(Screen Space Ray Tracing)
我们先来复习一下SSR
这边引用了这篇文章图形学渲染基础(8) 实时全局光照(Real-time Global illumination)
Screen Space Reflection(SSR) ,一类与 ray tracing 思路非常相似的屏幕空间GI方法,因此也有被叫为 Screen Space Ray Tracing(SSRT) 。它的想法是,将屏幕所看到的表面几何信息当成一个场景,然后计算间接光照时,往半球范围若干个方向投射射线,看看能和这个场景的哪个屏幕像素点相交,这些便可以相交的像素点便是提供间接光照的来源。
SSR 需要用到的屏幕信息:color、normal、depth
SSR 的算法流程:
- 在第一个 pass 只渲染整个场景的直接光照,得到包含直接光照结果的 color buffer 、normal buffer、 depth buffer。
- 在第二个 pass 对整个屏幕渲染,对于某个 shading point ,在该点往半球随机方向投射若干条射线(使用 ray marching算法),然后将与射线相交的点 p′将对 shading point 的间接光照做出贡献(这与渲染方程是一致的):其中当射线命中时, V=1 ;否则,V=0
为了减少计算,这里的L(p’,wi)项为间接光源提供的能量,仍然默认间接光照是diffuse的,因此式子可以简化成
SSR流程示意图如下:黄点是shading point,绿点是p’.
因为SSR是基于ray tracing的算法,因此可以根据不同的brdf反射不同的效果
在了解完SSR,我们可以将我们要实现的归为两步
- 光线求交
- 累加二次光照结果
bool RayMarch(vec3 ori, vec3 dir, out vec3 hitPos) {
//步长
float step=0.05;
//步数
const int stepNums=150;
//发射射线的方向
vec3 stepDir=normalize(dir);
//每次射线前进的距离
vec3 stepDistance=stepDir*step;
//从原点开始出发
vec3 curPos=ori;
for(int cursteps=0;cursteps<stepNums;cursteps++)
{
vec2 uv=GetScreenCoordinate(curPos);
//通过对比深度判断射线是否有交点
float rayDepth=GetDepth(curPos);
float gBufferDepth=GetGBufferDepth(uv);
//如果在一定范围内存在交点则执行下面逻辑
if(rayDepth-gBufferDepth>0.0001)
{
//通过hitPos传出该交点
hitPos=curPos;
return true;
}
curPos+=stepDistance;
}
return false;
}
上面算法为光线求交算法,用来判断一个像素点向dir方向发射的射线在一定步长内是否有交点。
间接光照
在实现了RayMarch后,我们可以通过采样的方式来将得到的二次光源的影响加入到原来的光照结果上了。
我们参考作业给的伪代码来实现
void main() {
float s = InitRand(gl_FragCoord.xy);
vec3 L = vec3(0.0);
vec3 worldPos=vPosWorld.xyz;
vec2 uv=GetScreenCoordinate(vPosWorld.xyz);
vec3 wi=normalize(uLightDir);
vec3 wo=normalize(uCameraPos-worldPos);
//直接光
L = EvalDiffuse(wi,wo,uv)*EvalDirectionalLight(uv);
//间接光
vec3 L_ind=vec3(0.0);
for(int i=0;i<SAMPLE_NUM;i++)
{
//pdf是用来限定采用范围的
float pdf;
vec3 localDir=SampleHemisphereCos(s,pdf);
vec3 normal=GetGBufferNormalWorld(uv);
//由于SampleHeisphereCos得到的是局部方向,要进行TBN变化到世界空间才能用
vec3 T,B;
LocalBasis(normal,T,B);
vec3 dir=normalize(mat3(T,B,normal)*localDir);
vec3 hitPos;
//交点记录到hitPos内
if(RayMarch(worldPos,dir,hitPos))
{
vec2 hitPos_uv=GetScreenCoordinate(hitPos);
L_ind+=EvalDiffuse(dir,wo,uv)/pdf*EvalDiffuse(wi,dir,hitPos_uv)*EvalDirectionalLight(hitPos_uv);
}
}
//累加直接光和间接光影响
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);
}
上面函数中,间接光的计算我们需要注意,框架中给的采样方法是用SampleHemisphereUniform(inout float s, out float pdf)或 SampleHemisphereCos(inout float s, out float pdf)方法来获取采样的方向和对应的PDF,前者是均匀采用,后者是按Cos加权采样,因为是对球面采样,所以我们选择用SampleHemisphereCos。
还有一点需要留意LocalBasis(vec3 n, out vec3 b1, out vec3 b2)函数是根据目前法线返回对应的T、B切向量的,因为用上面的采样函数得到的只是局部的方向向量,所以我们要经过TBN矩阵变换到世界空间才能正常计算。
最后一步,我们要切换光源和场景
//engine.js
// Cave
lightRadiance = [20, 20, 20];
lightPos = [-0.45, 5.40507, 0.637043];
lightDir = {
'x': 0.39048811,
'y': -0.89896828,
'z': 0.19843153,
};
//..
// Add shapes
//loadGLTF(renderer, 'assets/cube/', 'cube1', 'SSRMaterial');
//loadGLTF(renderer, 'assets/cube/', 'cube2', 'SSRMaterial');
loadGLTF(renderer, 'assets/cave/', 'cave', 'SSRMaterial');
参考文章:
图形学渲染基础(8) 实时全局光照(Real-time Global illumination)
GAMES202-作业3:Screen Space Ray Tracing
为什么PBR中Lambert光照要除PI?