Kagamine Len
文章20
标签10
分类2
作业7 微表面模型实现

作业7 微表面模型实现

由于作业八我不会写,所以这终于是Games101小作业最后一篇,后期还有可能会完成GAMES101一部分大作业。

这一部分基本上就是做阅读理解了,理解几个公式之后是较为简单的,我们可以认为菲涅尔项F和几何项G只是一个0到1的系数,而真正起决定性因素的是D项,D项需要满足在投影立体角下积分为1(可能是和概率密度有关的定义,这里留到Games202再理解),即$cos\theta * d\omega_h$下,而Cook-Torrance 模型的分母为给D项配平的系数。

公式最好参考learnOpengl,给出了最详细的描述。

下面先给出渲染的结果。

binary

首先在编码过程中,遇到了https://blog.csdn.net/Xuuuuuuuuuuu/article/details/129001805中提到的黑色噪点的问题,按照这篇文章解决就可以,白色噪点反而没有遇到,我认为在能量合理,且不存在浮点精度(如$f_r$项的分母过小)的情况下,是不会出现白色噪点的。

首先列出参考的所有网站,部分网站公式有问题,对于公式、公式内项的含义,建议参考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中提到的问题,即几何项把能量当做在遮挡过程中全部损失掉了,实际上这部分能量会在微表面上多次反射,因此实际上我们并不能简单的使用这个值,而是需要进行一个能量的回补。

image-20230417184231695

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

×