总览
本轮作业中,我们需要在一个光源为方向光,材质为漫反射 (Diffuse) 的场景中,完成屏幕空间下的全局光照效果(两次反射)。
为了在作业框架中实现上述效果,基于我们需要的信息不同我们会分三阶段着色,每个阶段都有相对应的任务。
- 第一次着色负责计算 Shadow Map 所需的深度值并保存到贴图中。
- 第二次着色负责计算屏幕空间中,每个像素对应的世界坐标系下位置、世界坐标系下法线、漫反射反射率和可见性信息并最终保存到对应贴图中。
- 第三次着色基于之前得到的场景几何信息 (像素对应的位置,法线),场景与光源的遮挡信息 (光源坐标系的深度值),场景的材质信息 (漫反射反射率),来计算两次反射的全局光照结果。
流程
对于本次实验,有如下三个任务点需要依次完成:
- 实现对场景直接光照的着色 (考虑阴影)。
- 实现屏幕空间下光线的求交 (SSR)。
- 实现对场景间接光照的着色。
如果你能确保每一步完成正确,最终我们可以得到质量不错的光照效果!
直接光照
熟悉如何使用提供的函数,以及GBuffer信息。
你需要实现位于 shaders/ssrShader/ssrFragment.glsl 文件中的 EvalDiffuse(wi, wo, uv) 和 EvalDirectionalLight(uv) 函数,并在 main 函数中实现直接光照的效果。
- 以下的 uv 表示着色点的屏幕空间坐标,即值域为 [0, 1]2。
- EvalDiffuse(wi, wo, uv) 函数的返回值为 BSDF1 的值。参数 wi 和 wo 为世界坐标系中的值,分别代表入射方向和出射方向,需要注意的是,这两个方向的起点都为着色点。在这一步中你会需要: 漫反射率和法线信息。我们提供了 GetGBufferDiffuse(uv) 和 GetGBufferNormalWorld(uv) 来获取漫反射率和法线。这里需要注意,GetGBufferDiffuse(uv) 返回的值是在线性空间的,GetGBufferNormalWorld(uv) 得到的法线是在世界坐标系的。为了获取屏幕空间的坐标,我们提供了 GetScreenCoordinate(posWorld) 函数,它的参数为世界坐标系中的位置。
BRDF = F G D / 4 (n,i) (n,o)
菲涅尔项、法线分布项、几何(遮蔽)项
输入:入射方向wi、出射方向wo、屏幕空间像素uv
菲涅尔项
有多少能量被反射取决于入射光的角度,如果入射角接近掠角grazing angle(入射方向和法线几乎垂直),这种情况下反射是最多的。
BRDF和光线传播都是可逆的。 入射和出射可以互换。菲涅尔项与PBR本身的性质有关(dielectric绝缘体和conductor导体) 菲涅尔项告诉我们有百分之多少的能量会被反射掉。
在物理上有非常复杂的表示,会考虑光线的s极化、p极化 考虑从空气到介质的折射率、考虑入射角。
vec3 F0 = GetGBufferDiffuse(uv);
vec3 h = normalize(wi + wo);
vec3 v = wi;
vec3 Fresnel_term = F0 + (vec3(1.0) - F0) * pow((vec3(1.0) - (h * v)),vec3(5.0));
发现有些vec3运算不需要强制设置vec3类型
简化后
vec3 F0 = GetGBufferDiffuse(uv);
vec3 h = normalize(wi + wo);
vec3 v = wi;
vec3 Fresnel_term = F0 + (1.0 - F0) * pow((1.0 - (h * v)),vec3(5.0));
法线分布项
如果微表面的法线分布集中,就说明微表面法线变化不明显,也就说明法线都差不多,也就是表面比较平坦。可以得出近似glossy,如果非常集中,就是specular。
如果把微表面的高度场拉大,等于所有面都被拉得倾斜了,使得normal的分布变化越来越明显。
vec3 alpha = F0;
vec3 n = GetGBufferNormalWorld(uv);
vec3 NDF_div = pow(pow(n * h,vec3(2.0)) * (pow(alpha,vec3(2.0)) - 1.0) + 1.0, vec3(2.0));
vec3 NDF_GGXTR = pow(alpha,vec3(2.0)) / (M_PI * NDF_div);
几何遮蔽项
解决微表面之间自遮挡的问题。 从光线出发,被遮挡叫shadowing;从眼睛(微表面)出发,被遮挡叫masking
经过自遮挡,微表面被遮挡的部分肯定不能那么亮,这一项就是为了进行变暗的操作。从上往下看基本不需要做这个操作,在grazing angle要让它变暗很多。
alpha = pow(( alpha + 1.0 ) * 0.5, vec3(2.0));
vec3 k = alpha * 0.5;
vec3 G1L = (n * wo) / ( (n * wo) * (1.0 - k) + k);
vec3 G1V = (n * wi) / ( (n * wi) * (1.0 - k) + k);
vec3 Graphic_term = G1L * G1V;
BRDF结果
BRDF = F G D / 4 (n,i) (n,o)
vec3 EvalDiffuse(vec3 wi, vec3 wo, vec2 uv) {
vec3 L = vec3(0.0);
// 菲涅尔项
vec3 F0 = GetGBufferDiffuse(uv);
vec3 h = normalize(wi + wo);
vec3 v = wi;
vec3 Fresnel_term = F0 + (1.0 - F0) * pow((1.0 - (h * v)),vec3(5.0));
// 法线分布项
vec3 alpha = F0;
vec3 n = GetGBufferNormalWorld(uv);
vec3 NDF_div = pow(pow(n * h,vec3(2.0)) * (pow(alpha,vec3(2.0)) - 1.0) + 1.0, vec3(2.0));
vec3 NDF_GGXTR = pow(alpha,vec3(2.0)) / (M_PI * NDF_div);
// 几何遮蔽项
alpha = pow(( alpha + 1.0 ) * 0.5, vec3(2.0));
vec3 k = alpha * 0.5;
vec3 G1L = (n * wo) / ( (n * wo) * (1.0 - k) + k);
vec3 G1V = (n * wi) / ( (n * wi) * (1.0 - k) + k);
vec3 Graphic_term = G1L * G1V;
// BRDF项
vec3 BRDF = Fresnel_term * Graphic_term * NDF_GGXTR / (4.0 * dot(n,wi) * dot(n,wo));
return BRDF;
}
- EvalDirectionalLight(uv) 函数的返回值为,着色点位于 uv 处得到的光源的辐射度,并且需要考虑遮挡关系 (Shadow Map)。在第二次着色的时候我们已经通过简单地比较深度得到了可见性信息,并保存到了贴图中。这里可以使用 GetGBufferuShadow(uv) 函数得到。你也可以修改 src/shaders/gbufferShader/gbufferFragment.glsl 文件中的 SimpleShadowMap(posWorld, bias) 来实现其他阴影算法。
这样拼出来的矩阵就是一个旋转矩阵。再做一个位移,网上的算法少一个位移运算所以有BUG
对比图
- 以下是我实现间接光的代码
#ifdef GL_ES
precision highp float;
#endif
uniform vec3 uLightDir;
uniform vec3 uCameraPos;
uniform vec3 uLightRadiance;
uniform sampler2D uGDiffuse;
uniform sampler2D uGDepth;
uniform sampler2D uGNormalWorld;
uniform sampler2D uGShadow;
uniform sampler2D uGPosWorld;
varying mat4 vWorldToScreen;
varying highp vec4 vPosWorld;
#define M_PI 3.1415926535897932384626433832795
#define TWO_PI 6.283185307
#define INV_PI 0.31830988618
#define INV_TWO_PI 0.15915494309
float Rand1(inout float p) {
p = fract(p * .1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
vec2 Rand2(inout float p) {
return vec2(Rand1(p), Rand1(p));
}
float InitRand(vec2 uv) {
vec3 p3 = fract(vec3(uv.xyx) * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec3 SampleHemisphereUniform(inout float s, out float pdf) {
vec2 uv = Rand2(s);
float z = uv.x;
float phi = uv.y * TWO_PI;
float sinTheta = sqrt(1.0 - z*z);
vec3 dir = vec3(sinTheta * cos(phi), sinTheta * sin(phi), z);
pdf = INV_TWO_PI;
return dir;
}
vec3 SampleHemisphereCos(inout float s, out float pdf) {
vec2 uv = Rand2(s);
float z = sqrt(1.0 - uv.x);
float phi = uv.y * TWO_PI;
float sinTheta = sqrt(uv.x);
vec3 dir = vec3(sinTheta * cos(phi), sinTheta * sin(phi), z);
pdf = z * INV_PI;
return dir;
}
void LocalBasis(vec3 n, out vec3 b1, out vec3 b2) {
float sign_ = sign(n.z);
if (n.z == 0.0) {
sign_ = 1.0;
}
float a = -1.0 / (sign_ + n.z);
float b = n.x * n.y * a;
b1 = vec3(1.0 + sign_ * n.x * n.x * a, sign_ * b, -sign_ * n.x);
b2 = vec3(b, sign_ + n.y * n.y * a, -n.y);
}
vec4 Project(vec4 a) {
return a / a.w;
}
float GetDepth(vec3 posWorld) {
float depth = (vWorldToScreen * vec4(posWorld, 1.0)).w;
return depth;
}
/*
* Transform point from world space to screen space([0, 1] x [0, 1])
*
*/
vec2 GetScreenCoordinate(vec3 posWorld) {
vec2 uv = Project(vWorldToScreen * vec4(posWorld, 1.0)).xy * 0.5 + 0.5;
return uv;
}
float GetGBufferDepth(vec2 uv) {
float depth = texture2D(uGDepth, uv).x;
if (depth < 1e-2) {
depth = 1000.0;
}
return depth;
}
vec3 GetGBufferNormalWorld(vec2 uv) {
vec3 normal = texture2D(uGNormalWorld, uv).xyz;
return normal;
}
vec3 GetGBufferPosWorld(vec2 uv) {
vec3 posWorld = texture2D(uGPosWorld, uv).xyz;
return posWorld;
}
float GetGBufferuShadow(vec2 uv) {
float visibility = texture2D(uGShadow, uv).x;
return visibility;
}
vec3 GetGBufferDiffuse(vec2 uv) {
vec3 diffuse = texture2D(uGDiffuse, uv).xyz;
diffuse = pow(diffuse, vec3(2.2));
return diffuse;
}
/*
* Evaluate diffuse bsdf value.
*
* wi, wo are all in world space.
* uv is in screen space, [0, 1] x [0, 1].
*
*/
vec3 EvalDiffuse(vec3 wi, vec3 wo, vec2 uv) {
vec3 L = vec3(0.0);
// 菲涅尔项
float F0 = 1.;
vec3 h = normalize(wi + wo);
vec3 v = wi;
float Fresnel_term = F0 + (1.0 - F0) * pow((1.0 - dot(h,v)),5.);
// 法线分布项
float alpha = F0;
vec3 n = GetGBufferNormalWorld(uv);
float NDF_div = pow(pow(dot(n,h),2.) * (pow(alpha,2.) - 1.) + 1., 2.);
float NDF_GGXTR = pow(alpha,2.) / (M_PI * NDF_div);
// 几何遮蔽项
alpha = pow(( alpha + 1.0 ) * 0.5, 2.);
float k = alpha * 0.5;
float G1L = dot(n,wo) / ( dot(n,wo) * (1.0 - k) + k);
float G1V = dot(n,wi) / ( dot(n,wi) * (1.0 - k) + k);
float Graphic_term = G1L * G1V;
// BRDF项
vec3 BRDF = GetGBufferDiffuse(uv) * Fresnel_term * Graphic_term * NDF_GGXTR / (4.0 * dot(n,wi) * dot(n,wo));
return BRDF;
}
/*
* Evaluate directional light with shadow map
* uv is in screen space, [0, 1] x [0, 1].
*
*/
vec3 EvalDirectionalLight(vec2 uv) {
vec3 Le = vec3(0.);
Le = vec3(GetGBufferuShadow(uv));
return Le;
}
// 光线前进的时候可能会离开屏幕空间,这样GetGBufferDepth计算出的结果有问题
// 这种情况下uv会离开[0,1]空间
bool outScreen(vec3 pos){
vec2 uv = GetScreenCoordinate(pos);
return any(bvec4(lessThan(uv, vec2(0.0)), greaterThan(uv, vec2(1.0))));
}
// 这种情况下,光线前进走到了深度图的物体前面,并没有产生碰撞
// 我们需要判断,一开始在深度图外面而不是深度图上面(起始点就是深度图上的点)
// 下一步会进入深度图里面
bool atFront(vec3 pos){
return GetDepth(pos) < GetGBufferDepth(GetScreenCoordinate(pos));
}
bool RayMarch(vec3 ori, vec3 dir, out vec3 hitPos) {
hitPos = vec3(0.);
float step = 0.8;
vec3 wpos = ori;
bool f_intersect = false;
for (int i = 0;i < 20;i ++){
vec2 uv = GetScreenCoordinate(wpos);
float depth = GetGBufferDepth(uv);
float distance_ray = GetDepth(wpos);
if (outScreen(wpos)){
break;
}
if (atFront(wpos + dir * step)){
wpos += dir * step;
}
else {
f_intersect = true;
// 步子已经离得很近了
if (step < 1e-2){
// 判断当前在深度图前面
float d1 = GetGBufferDepth(GetScreenCoordinate(wpos)) - GetDepth(wpos); // + 1e-2
// 判断下一步在深度图后面
float d2 = GetDepth(wpos + dir * step) - GetGBufferDepth(GetScreenCoordinate(wpos + dir * step)); // + 1e-2
if(d1 > 0. && d2 > 0.){
hitPos = wpos + dir * step; // * d1 / (d1 + d2)
return true;
}
// break;
}
}
// 探测到下一步会走进去,开始慢慢走
if(f_intersect){
step *= 0.5;
}
}
return false;
}
mat4 GetViewport(int x,int y,int w,int h){
mat2 rx = mat2(-1.,x,1,x + w);
mat2 ry = mat2(-1.,y,1,y + h);
return mat4(1.);
}
#define SAMPLE_NUM 10
void main() {
float s = InitRand(gl_FragCoord.xy);
vec3 L = vec3(0.0);
// 直接取sRGB色彩空间的贴图,进行gamma校正
// L = GetGBufferDiffuse(GetScreenCoordinate(vPosWorld.xyz));
// 直接取直接光照的shadowmap 和 贴图合并处理
// L = GetGBufferDiffuse(GetScreenCoordinate(vPosWorld.xyz)) * EvalDirectionalLight(GetScreenCoordinate(vPosWorld.xyz),vPosWorld.xyz) * uLightRadiance;
// 间接光照 外界光打到pass1物体上,然后传递到直接光照物体,最后传递到camera
vec2 uv = GetScreenCoordinate(vPosWorld.xyz);
vec3 bufferNormal = GetGBufferNormalWorld(uv);
vec3 b1,b2;
// 把b1作为x轴 b2作为y轴 法线作为z轴
LocalBasis(bufferNormal, b1, b2);
// 每个轴都可以用其他轴表示出来
mat3 worldToLocal = mat3(b1, b2, bufferNormal);
vec3 worldPos = vPosWorld.xyz;
vec3 light2Cam = uCameraPos - worldPos;
vec3 light2Obj = bufferNormal - light2Cam;
light2Cam = normalize(light2Cam);
light2Obj = normalize(light2Obj);
bool happen_bounce = false;
for (int i = 0;i < SAMPLE_NUM; ++i){
float pdf = 0.;
vec3 hitPos;
vec3 dir = normalize(worldToLocal * SampleHemisphereCos(s,pdf));
if (dot(light2Cam, bufferNormal) > 0. && RayMarch(worldPos,dir,hitPos)){
vec2 uv1 = GetScreenCoordinate(hitPos);
vec3 Lightpath2 = EvalDirectionalLight(uv1);
vec3 Lightpath = vec3(0.);
Lightpath += GetGBufferDiffuse(uv1) * Lightpath2;
L += Lightpath * uLightRadiance;
happen_bounce = true;
}
}
L /= float(SAMPLE_NUM) * TWO_PI;
L += EvalDiffuse(light2Obj,light2Cam,uv) * EvalDirectionalLight(uv) * uLightRadiance;
if (happen_bounce){
L /= 2.;
}
vec3 color = pow(clamp(L, vec3(0.0), vec3(1.0)), vec3(1.0 / 2.2));
gl_FragColor = vec4(vec3(color.rgb), 1.0);
}
BSDF可以认为包含了BRDF和BTDF,BRDF是反射而BTDF是透射。 ↩︎