任务
为场景实现屏幕空间的全局光照效果
1.直接光照: 实现ssrFragmentShader中的EvalDiffuse(wi, wo, uv) 和EvalDirectionalLight(uv) 函数,并在 main 函数中实现直接光照的效果。
2.屏幕空间光线求交:实现RayMarch(ori, dir, out hitPos) 函数。RayMarch 函数的返回值为是否相交,当相交的时候需要将参数 hitPos设置为交点。参数 ori 和 dir 为世界坐标系中的值,分别代表光线的起点和方向,其中方向向量为单位向量。
3.间接光照:在 main 函数中实现间接光照,使用蒙特卡洛方法求解渲染方程。
实现
EvalDiffuse
该函数是计算diffuse材质的BSDF的,功能简单,其实就是之前101的内容。这里将EvalDiffuse看作渲染方程的f,但是由于计算间接光照的伪代码没有给出cos项,因此这里多乘以一个cos项。另外提一下,这里的INV_PI是PI的倒数,因为在计算机里乘法计算比除法计算更快,因此预定义一个INV_PI能加速渲染。
vec3 EvalDiffuse(vec3 wi, vec3 wo, vec2 uv) {
vec3 diffuse = GetGBufferDiffuse(uv);
vec3 normal = GetGBufferNormalWorld(uv);
float cos = max(0., dot(normal, wi));
return diffuse * cos * INV_PI;
}
EvalDirectionLight
直接使用题目提供的API,简短两行
vec3 EvalDirectionalLight(vec2 uv) {
float visibility = GetGBufferuShadow(uv);
return visibility * uLightRadiance;
}
RayMarch
按照202课上的原理,在着色点的位置开始,以某个方向射出光线,一步步向前试探找到交点。有交点的条件是:试探的点的深度比屏幕空间的该位置的深度更深(也就是在屏幕空间一直走直到被遮挡住,这时候说明有交点)。
bool RayMarch(vec3 ori, vec3 dir, out vec3 hitPos) {
//定义一个最大步数防止没有找到交点而无限前进
const int maxStep = 100;
float stepSize = 0.05;
vec3 stepDir = normalize(dir) * stepSize;
for(int step=0; step< maxStep ; step++){
float depth = GetDepth(ori);
vec2 screenUV = GetScreenCoordinate(ori);
float screenDepth = GetGBufferDepth(screenUV);
if(depth > screenDepth + 0.0001){
hitPos = ori;
return true;
}
ori += dir;
}
return false;
}
main里用蒙特卡洛方法解渲染方程
这里渲染方程没有cos项,cos项在上面的EvalDiffuse已经实现。
将其转化为代码,这里有需要注意的地方就是direction是局部坐标系的。在101中的作业3里做bumper时,我们使用TBN矩阵来将切线坐标系转化为世界坐标系。将切线坐标系转化为世界坐标系其实还需要uv坐标来确定,因为需要知道某个三角形在纹理上的位置。而这里只涉及方向的转化不涉及位置,所以直接使用TBN矩阵就能获得direction转化到世界坐标的朝向。作业框架里给我们提供了LocalBasis函数来获取两个切线。因此获取坐标系的三个方向后,使用它们创建一个TBN矩阵。
vec3 inDirectLight = vec3(0.0);
for(int i =0;i<SAMPLE_NUM;i++){
float pdf;
vec3 direction = SampleHemisphereUniform(s,pdf);
vec3 b1,b2;
LocalBasis(normal,b1,b2);
mat3 rotateMatrix = mat3(normal,b1,b2);
direction = normalize(rotateMatrix * direction);
vec3 hitPos;
if(RayMarch(pos,direction,hitPos)){
vec2 hitUV = GetScreenCoordinate(hitPos);
inDirectLight += EvalDiffuse(direction,wo,uv)/pdf * EvalDiffuse(wi,direction,hitUV) * EvalDirectionalLight(hitUV);
}
}
inDirectLight /= float(SAMPLE_NUM);
L += inDirectLight;
整个的main代码
vec3 EvalReflect(vec3 wi,vec3 wo,vec2 uv){
vec3 pos = GetGBufferPosWorld(uv);
vec3 normal = GetGBufferNormalWorld(uv);
vec3 dir = normalize(reflect(-wo,normal) );
vec3 hitPos;
if(RayMarch(pos,dir,hitPos)){
vec2 hitUV = GetScreenCoordinate(hitPos);
return GetGBufferDiffuse(hitUV);
}
return vec3(0.0,0.0,0.0);
}
#define SAMPLE_NUM 3
void main() {
float s = InitRand(gl_FragCoord.xy);
vec3 pos = vPosWorld.xyz;
vec2 uv = GetScreenCoordinate(pos);
vec3 wi = normalize(uLightDir);
vec3 wo = normalize(uCameraPos - pos );
vec3 normal = GetGBufferNormalWorld(uv);
//vec3 L = vec3(0.0);
//L = GetGBufferDiffuse(GetScreenCoordinate(vPosWorld.xyz)); //初始的无阴影着色
vec3 L = EvalDiffuse(wi,wo,uv) * EvalDirectionalLight(uv); //用于调试带阴影的着色
//L = ( L + EvalReflect(wi,wo,uv) ) / 2.0; //用于调试反射
vec3 inDirectLight = vec3(0.0);
for(int i =0;i<SAMPLE_NUM;i++){
float pdf;
vec3 direction = SampleHemisphereUniform(s,pdf);
vec3 b1,b2;
LocalBasis(normal,b1,b2);
mat3 rotateMatrix = mat3(normal,b1,b2);
direction = normalize(rotateMatrix * direction);
vec3 hitPos;
if(RayMarch(pos,direction,hitPos)){
vec2 hitUV = GetScreenCoordinate(hitPos);
inDirectLight += EvalDiffuse(direction,wo,uv)/pdf * EvalDiffuse(wi,direction,hitUV) * EvalDirectionalLight(hitUV);
}
}
inDirectLight /= float(SAMPLE_NUM);
L += inDirectLight;
vec3 color = pow(clamp(L, vec3(0.0), vec3(1.0)), vec3(1.0 / 2.2));
gl_FragColor = vec4(vec3(color.rgb), 1.0);
}
结果
无阴影的结果
含阴影的结果
调试反射的结果,上面彩色格子的是正方体的面,这里反射的是地板的光。下面灰色的是地板面,反射的是正方体的光照。
调试间接光照的结果,这里在engine里将cube1换成了cube2。调试cave时,cave是有自己的摄像机属性和光源属性的,需要额外调一下。
原来硬阴影的cube2
间接光照的cube2
硬阴影的cave
实现了间接光照的cave
bonus部分没做了,原理不会很难,就是对RayMarch的优化。但是看其他大佬的作业,大家需要改动的地方特别多,最近没什么时间就没去做了= =