作业7 微表面模型实现
由于作业八我不会写,所以这终于是Games101小作业最后一篇,后期还有可能会完成GAMES101一部分大作业。
这一部分基本上就是做阅读理解了,理解几个公式之后是较为简单的,我们可以认为菲涅尔项F和几何项G只是一个0到1的系数,而真正起决定性因素的是D项,D项需要满足在投影立体角下积分为1(可能是和概率密度有关的定义,这里留到Games202再理解),即$cos\theta * d\omega_h$下,而Cook-Torrance 模型的分母为给D项配平的系数。
公式最好参考learnOpengl,给出了最详细的描述。
下面先给出渲染的结果。

首先列出参考的所有网站,部分网站公式有问题,对于公式、公式内项的含义,建议参考learnopengl,推导部分则惨遭其他网页。
代码最好参考learnopengl:https://learnopengl-cn.github.io/07%20PBR/02%20Lighting/
微表面模型推导:https://zhuanlan.zhihu.com/p/434964126
https://www.cnblogs.com/wickedpriest/p/13361667.html
https://zhuanlan.zhihu.com/p/152226698
https://www.jianshu.com/p/d70ee9d4180e/
https://www.jianshu.com/p/d70ee9d4180e/
菲涅尔项:https://zhuanlan.zhihu.com/p/461531682
Vector3f Material::eval(const Vector3f &wi, const Vector3f &wo, const Vector3f &N){
switch(m_type){
case MICRO_FACET:
{
float cosalpha = dotProduct(N, wo);
if(cosalpha < 0 ){
return Vector3f(0.0f);
}
auto h = (wi + wo).normalized();
auto F_function=[](const Vector3f & F0,const Vector3f &h, const Vector3f &v){
return F0 + (Vector3f(1.0f)-F0) * pow(1- dotProduct(h,v),5);
};
auto f = F_function(Ks,h,wi);
float roughness = 0.25f;
auto G_function =[](const float &roughness,const Vector3f &l,const Vector3f &v,const Vector3f &n){
auto k = (roughness + 1.f) * (roughness + 1.f)/8.f;
auto ndotv = dotProduct(n,v);
auto ndotl = dotProduct(n,l);
return (ndotv/(ndotv * (1-k) + k)) * (ndotl/(ndotl * (1-k) + k));
};
auto g = G_function(roughness,wi,wo,N);
auto D_funcion = [](const float &roughness, const Vector3f &h, const Vector3f &n){
auto alpha2 = roughness * roughness;
auto cos_theta = dotProduct(n,h);
float div = M_PI * pow((cos_theta * cos_theta)*(alpha2 - 1) + 1,2);
return alpha2 / div;
};
auto d = D_funcion(roughness,h,N);
auto diffuse = Kd / M_PI;
auto div = 4 * dotProduct(N, wo) * dotProduct(N, wi);
auto specular = f * g * d / div;
auto specular_clamp = Vector3f(std::min(1.0f,specular.x),std::min(1.0f,specular.y),std::min(1.0f,specular.z));
return diffuse + specular_clamp;
}
case DIFFUSE:
{
// calculate the contribution of diffuse model
float cosalpha = dotProduct(N, wo);
if (cosalpha > 0.0f) {
Vector3f diffuse = Kd / M_PI;
return diffuse;
}
else
return Vector3f(0.0f);
break;
}
}
}
首先是代码中的两个问题,第一是不要忘记判断cosalpha的值,避免出现全黑的情况(能量负的太多了)
其次是最后的计算specular的时候,按照learnopengl的做法,应该是在分母加上一个比较小的值来确保分母不为负数,我这里直接对specluar_clamp到了(1,1,1)以内,实际上这么做应该是不正确的,不过最后的效果没有什么问题,就没有改正了。
首先就是大家都提及不多的总公式:
大量文章中都详细讲解了specular项的系数和lambert项的系数,却没有介绍$K_s/K_d$的值的含义,只是通俗的认为他是这两项的一个强度,首先我们需要知道的是,为了保持能量守恒$K_d + K_s \le 1$(这两个是系数,实际上后后面两个f保证了能量的积分输出是小于等于输入的)。
具体推导我们这里就不看了,我认为有很多的数学上的定义还没有搞明白。
在learnopengl中我们有提到:https://learnopengl-cn.github.io/07%20PBR/01%20Theory/
首先我们需要知道,导体、电介质和半导体之间是有区别的,菲涅尔方程还存在一些细微的问题。其中一个问题是Fresnel-Schlick近似仅仅对电介质或者说非金属表面有定义。对于导体(Conductor)表面(金属),使用它们的折射指数计算(ior)基础折射率(F0)并不能得出正确的结果,这样我们就需要使用一种不同的菲涅尔方程来对导体表面进行计算。
可以通过frsnel-schlick公式看出,F0为$h*v=1$时候的值,即h和v平行,即入射角等于出射角等于半程向量。 即观察方向等于入射方向时(观察到物体本来的颜色F0)。
在导体情况下,我们无法通过ior求出F0,且在导体情况下,打入导体的光全部被吸收,Kd为0。
在电介质情况下,F0可以通过ior求出,且Kd = 1- Ks。
对于半导体(或者说混合介质),我们定义金属度,(可以看出对于金属surfaceColor = f0)
vec3 F0 = vec3(0.04);
F0 = mix(F0, surfaceColor.rgb, metalness);
对于材料的更多解释:https://zhuanlan.zhihu.com/p/21961722
我们需要注意:
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo, metallic);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
vec3 Ks = F; //存疑 =F0?
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - metallic; //根据metallic
Lo += (kD * albedo / PI + specular) * radiance * NdotL;
可以看到,F值就代表了Ks的值,且在最后的运算中specular无需再重复计算*Ks。
接下来在opengl中进行了gamma矫正,而我们这里使用了sRGB颜色,没有进行Gamma矫正。
还需要注意一个在Games202中提到的问题,即几何项把能量当做在遮挡过程中全部损失掉了,实际上这部分能量会在微表面上多次反射,因此实际上我们并不能简单的使用这个值,而是需要进行一个能量的回补。

games101系列总算是看完了,还剩下大作业可能会在之后做一些项目,然后还有C++/shader/opengl/vk(106)只能说任重而道远。
