反射模型章节中,介绍了渲染时光照反射的计算原理,包括镜面反射定律折射定律粗糙度和微平面理论等,并且给出了一个简单的漫反射(Diffuse Reflection)示例。由于篇幅原因,将常用的反射模型介绍拆分到这里,示例代码来源于PBRT书籍。
BxDF接口设计中提到过,一个反射模型至少需要包含3个函数,f(在知道入射方向和出射方向的情况下计算BxDF的值)、Sample_f(在知道出射方向的情况下采样入射方向,并计算BxDF的值),PDF(该BxDF的分布函数)。
下面介绍一些常用的反射模型。

导体BRDF

金属导体有一个物理性质,进入金属的光线会被完全吸收,不会发生散射,所以金属导体只需要考虑反射即可,这也是导体使用的反射模型是BRDF的原因。

class ConductorBxDF {
  public:
    ...
  private:
    TrowbridgeReitzDistribution mfDistrib;
    SampledSpectrum eta, k;
};

其中有两个成员变量,mfDistrib描述粗糙度和微平面的分布,eta, k是折射率和衰减率,它们会被当做一个复数的实部和虚部来处理,在导体的菲涅尔方程有说明。

Sample_f

pstd::optional<BSDFSample>
Sample_f(Vector3f wo, Float uc, Point2f u, TransportMode mode,
         BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const {
    if (!(sampleFlags & BxDFReflTransFlags::Reflection)) return {};
    if (mfDistrib.EffectivelySmooth()) {
        // 镜面反射 
        Vector3f wi(-wo.x, -wo.y, wo.z);
        SampledSpectrum f = FrComplex(AbsCosTheta(wi), eta, k) / AbsCosTheta(wi);
        return BSDFSample(f, wi, 1, BxDFFlags::SpecularReflection);
    }
    // 微平面采样
    Vector3f wm = mfDistrib.Sample_wm(wo, u);
    Vector3f wi = Reflect(wo, wm);
    if (!SameHemisphere(wo, wi)) return {};
 
    // 微平面的 pdf 
    Float pdf = mfDistrib.PDF(wo, wm) / (4 * AbsDot(wo, wm));
 
    // 计算BRDF值
    Float cosTheta_o = AbsCosTheta(wo), cosTheta_i = AbsCosTheta(wi);
    SampledSpectrum F = FrComplex(AbsDot(wo, wm), eta, k);
 
    SampledSpectrum f = mfDistrib.D(wm) * F * mfDistrib.G(wo, wi) /
                           (4 * cosTheta_i * cosTheta_o);
    return BSDFSample(f, wi, pdf, BxDFFlags::GlossyReflection);
}

镜面但反射部分:

        Vector3f wi(-wo.x, -wo.y, wo.z);
        SampledSpectrum f = FrComplex(AbsCosTheta(wi), eta, k) / AbsCosTheta(wi);
        return BSDFSample(f, wi, 1, BxDFFlags::SpecularReflection);

由于计算时所有的方向都被转换到法线-切线的坐标空间,所以反射方向等于(-wo.x, -wo.y, wo.z),不需要使用反射定律的函数计算了。
镜面反射只在反射方向上贡献光照效果,所以它的pdf是一个分布函数,按照pdf的定义,它应该在0处(观察方向等于反射方向时)为无穷大,在其余地方为0。但是在使用时,“无穷大”的概念必然是要从计算机中程序规避的,所以这里设置pdf为1(相当于只有这种情况对积分有贡献)。在根据蒙特卡洛积分的计算,单次采样时有

由于是完美镜面反射,两边的是相等的,于是

也就是上面的计算方法。在完美镜面情况下,透射与反射的计算方式相同。
后续的微平面采样、pdf计算、BRDF使用的都是Torrance–Sparrow模型中的方法。

f

SampledSpectrum f(Vector3f wo, Vector3f wi, TransportMode mode) const {
    if (!SameHemisphere(wo, wi)) return {};
    // 镜面反射时这个函数不提供任何值
    if (mfDistrib.EffectivelySmooth()) return {};
    
    // 在已知wi和wo的情况下计算wm
    Float cosTheta_o = AbsCosTheta(wo), cosTheta_i = AbsCosTheta(wi);
    if (cosTheta_i == 0 || cosTheta_o == 0) return {};
    Vector3f wm = wi + wo;
    if (LengthSquared(wm) == 0) return {};
    wm = Normalize(wm);
 
    // 和上面相同的BRDF计算方法 
    SampledSpectrum F = FrComplex(AbsDot(wo, wm), eta, k);
 
    return mfDistrib.D(wm) * F * mfDistrib.G(wo, wi) /
              (4 * cosTheta_i * cosTheta_o);
}

这里wm和BRDF的计算方式在Torrance–Sparrow模型中也介绍过。
重点在于镜面反射,当发生镜面反射时这个函数不提供任何值,按照上面的结论,它这里提供的pdf应该是0,但是在计算机程序中,后面使用采样结果除以pdf时必然会除以0。所以这里按照镜面反射的物理定义,当观察方向与反射方向不一致时(只要不是严格的从wo使用反射定律计算wi,都视为不一致),它的光照贡献都为0,所以这里直接返回”空值”。

pdf

Float PDF(Vector3f wo, Vector3f wi, TransportMode mode,
          BxDFReflTransFlags sampleFlags) const {
    if (!(sampleFlags & BxDFReflTransFlags::Reflection)) return 0;
    if (!SameHemisphere(wo, wi)) return 0;
    if (mfDistrib.EffectivelySmooth()) return 0;
    
       Vector3f wm = wo + wi;
       if (LengthSquared(wm) == 0) return 0;
       wm = FaceForward(Normalize(wm), Normal3f(0, 0, 1)); 
       return mfDistrib.PDF(wo, wm) / (4 * AbsDot(wo, wm));
}
 
template <typename T>
Normal3<T> FaceForward(Normal3<T> n, Vector3<T> v) {
    return (Dot(n, v) < 0.f) ? -n : n;
}

pdf的计算方式上面已介绍。

绝缘体BSDF

相比于金属导体,光线在绝缘体上同时会发生反射和透射,所以它的反射模型也更加复杂。

class DielectricBxDF {
  public:
    ...
  private:
    Float eta;
    TrowbridgeReitzDistribution mfDistrib;
};

绝缘体不需要考虑光线在物体内部的散射效果,所以只有折射率。

Sample_f

光滑的绝缘体

pstd::optional<BSDFSample>
DielectricBxDF::Sample_f(Vector3f wo, Float uc, Point2f u,
        TransportMode mode, BxDFReflTransFlags sampleFlags) const {
    if (eta == 1 || mfDistrib.EffectivelySmooth()) {
        // 镜面反射
        Float R = FrDielectric(CosTheta(wo), eta), T = 1 - R;
        // 计算发生反射和透射的概率
        Float pr = R, pt = T;
        if (!(sampleFlags & BxDFReflTransFlags::Reflection)) pr = 0;
        if (!(sampleFlags & BxDFReflTransFlags::Transmission)) pt = 0;
        if (pr == 0 && pt == 0)
            return {};
 
        if (uc < pr / (pr + pt)) {
            Vector3f wi(-wo.x, -wo.y, wo.z);
            SampledSpectrum fr(R / AbsCosTheta(wi));
            return BSDFSample(fr, wi, pr / (pr + pt), BxDFFlags::SpecularReflection);
        } else {
            Vector3f wi;
            Float etap;
            bool valid = Refract(wo, Normal3f(0, 0, 1), eta, &etap, &wi);
            if (!valid) return {};
 
            SampledSpectrum ft(T / AbsCosTheta(wi));
            // 考虑非对称的散射和折射
            if (mode == TransportMode::Radiance)
                ft /= Sqr(etap);
 
            return BSDFSample(ft, wi, pt / (pr + pt),
                              BxDFFlags::SpecularTransmission, etap);
        }
    } else {
        ...
    }
}

光滑的绝缘体不需要考虑微表面分布,反射部分是镜面反射。
通过折射率计算反射和透射光线的能量比例,

        Float R = FrDielectric(CosTheta(wo), eta), T = 1 - R;
        Float pr = R, pt = T;

使用菲涅尔方程中的方法计算出反射能量占比R,在计算出透射能量占比T,以它们作为概率决定此次采样是反射还是透射。
镜面反射的计算方式和上面金属BRDF的一致,唯一的区别就是这次的pdf = pr / (pr + pt),也就是发生反射的概率。
透射部分使用了折射定律介绍的方法,

        bool valid = Refract(wo, Normal3f(0, 0, 1), eta, &etap, &wi);
        if (!valid) return {};

这里的输出器etap和wi,wi是折射的方向,etap是折射率,在正常情况下etap=eta,只有发生的反向的折射时(即入射方向和法线不在同一侧,光线由物体内部射出),etap=1 / eta。
下面折射光线BSDF值的计算也和反射一样,但是多了一个步骤:

        if (mode == TransportMode::Radiance)
            ft /= Sqr(etap);

这里是考虑到真实世界的透射,透射是不对称的,当发生透射后,透射出的能量会出现衰减,

具体的关系为:

因为代码中,所以这里计算Li时要除以sqr(eta)。
这种透射方式对路径追踪来说很不友好,因为整个路径追踪算法都是按照光路是可逆的这一定律来设计的,所以在一些渲染系统中,会直接忽悠掉透射能量的衰减,这里也加了条件,TransportMode为Radiance时才会考虑不对称透射的情况。

粗糙的绝缘体

粗糙的绝缘体在反射时需要考虑微平面理论,并且在透射时计算方式也有差异。

pstd::optional<BSDFSample>
DielectricBxDF::Sample_f(Vector3f wo, Float uc, Point2f u,
        TransportMode mode, BxDFReflTransFlags sampleFlags) const {
        ...
        }
    } else {
        // 采样微平面法线
        Vector3f wm = mfDistrib.Sample_wm(wo, u);
        Float R = FrDielectric(Dot(wo, wm), eta);
        Float T = 1 - R;
 
        Float pr = R, pt = T;
        if (!(sampleFlags & BxDFReflTransFlags::Reflection)) pr = 0;
        if (!(sampleFlags & BxDFReflTransFlags::Transmission)) pt = 0;
        if (pr == 0 && pt == 0)
            return {};
 
        Float pdf;
        if (uc < pr / (pr + pt)) {
            Vector3f wi = Reflect(wo, wm);
            if (!SameHemisphere(wo, wi)) return {};
            pdf = mfDistrib.PDF(wo, wm) / (4 * AbsDot(wo, wm)) * pr / (pr + pt);
            
            SampledSpectrum f(mfDistrib.D(wm) * mfDistrib.G(wo, wi) * R /
                                    (4 * CosTheta(wi) * CosTheta(wo)));
            return BSDFSample(f, wi, pdf, BxDFFlags::GlossyReflection);
        } else {
            Float etap;
            Vector3f wi;
            bool tir = !Refract(wo, (Normal3f)wm, eta, &etap, &wi);
            if (SameHemisphere(wo, wi) || wi.z == 0 || tir)
                return {};
 
            Float denom = Sqr(Dot(wi, wm) + Dot(wo, wm) / etap);
            Float dwm_dwi = AbsDot(wi, wm) / denom;
            pdf = mfDistrib.PDF(wo, wm) * dwm_dwi * pt / (pr + pt);
 
            SampledSpectrum ft(T * mfDistrib.D(wm) * mfDistrib.G(wo, wi) *
                         std::abs(Dot(wi, wm) * Dot(wo, wm) /
                         (CosTheta(wi) * CosTheta(wo) * denom)));
 
            if (mode == TransportMode::Radiance)
                    ft /= Sqr(etap);
 
            return BSDFSample(ft, wi, pdf, BxDFFlags::GlossyTransmission, etap);
        }
    }
}

反射的部分与粗糙金属导体材质的反射相似,增加了一个反射概率pr / (pr + pt)到pdf。
主要区别在与透射部分。
第一个区别,不在考虑全反射的透射,上面的光滑绝缘体允许Refract函数计算出的wi和wo在同一侧,而这里当它们位于同一侧是直接返回空。

            bool tir = !Refract(wo, (Normal3f)wm, eta, &etap, &wi);
            if (SameHemisphere(wo, wi) || wi.z == 0 || tir)
                return {};

第二,BSDF的计算方式发生改变,由于反射和透射的区别,计算BSDF时不能继续按照半角向量中使用的方法,其的关系发生了改变,具体的关系为:

于是PDF中计算BSDF的pdf就变成了

对应的代码:

            Float denom = Sqr(Dot(wi, wm) + Dot(wo, wm) / etap);
            Float dwm_dwi = AbsDot(wi, wm) / denom;
            pdf = mfDistrib.PDF(wo, wm) * dwm_dwi * pt / (pr + pt);

BSDF值的计算由于pdf的改变也发生改变,具体的推导过程与Torrance–Sparrow BRDF相似,这里直接给出结果:

对应的代码

            SampledSpectrum ft(T * mfDistrib.D(wm) * mfDistrib.G(wo, wi) *
                         std::abs(Dot(wi, wm) * Dot(wo, wm) /
                         (CosTheta(wi) * CosTheta(wo) * denom)));

f

这里也发光滑和粗糙绝缘体,和光滑金属一样,光滑绝缘体f返回0。

SampledSpectrum DielectricBxDF::f(Vector3f wo, Vector3f wi,
                                  TransportMode mode) const {
    if (eta == 1 || mfDistrib.EffectivelySmooth())
        return SampledSpectrum(0.f);
 
    Float cosTheta_o = CosTheta(wo), cosTheta_i = CosTheta(wi);
    // cosTheta同号,证明wi和wo在同一侧,发生反射
    bool reflect = cosTheta_i * cosTheta_o > 0;
    float etap = 1;
    // 如果发生透射,确认透射的方向
    if (!reflect)
        etap = cosTheta_o > 0 ? eta : (1 / eta);
    // 计算wm
    Vector3f wm = wi * etap + wo;
    if (cosTheta_i == 0 || cosTheta_o == 0 || LengthSquared(wm) == 0) return {};
    wm = FaceForward(Normalize(wm), Normal3f(0, 0, 1));
 
    // 忽略背向微平面
    if (Dot(wm, wi) * cosTheta_i < 0 || Dot(wm, wo) * cosTheta_o < 0)
        return {};
 
    Float F = FrDielectric(Dot(wo, wm), eta);
    if (reflect) {
        // 反射BSDF
        return SampledSpectrum(mfDistrib.D(wm) * mfDistrib.G(wo, wi) * F /
                                     std::abs(4 * cosTheta_i * cosTheta_o));
    } else {
        // 透射BSDF
        Float denom = Sqr(Dot(wi, wm) + Dot(wo, wm)/etap) * cosTheta_i * cosTheta_o;
        Float ft = mfDistrib.D(wm) * (1 - F) * mfDistrib.G(wo, wi) *
                         std::abs(Dot(wi, wm) * Dot(wo, wm) / denom);
        if (mode == TransportMode::Radiance)
            ft /= Sqr(etap);
 
        return SampledSpectrum(ft);
    }
}

这里的重点在于wm的计算,根据折射定律,wm的计算规则如下,

使用公式表示:

公式里的与图中是相反的,因为图里面是按照正常的光路,为入射方向,为出射方向,但是在PBRT的光线追踪里面,光路是相反的,所以这两个方向的含义是相反的。
计算wm的方式:

    Vector3f wm = wi * etap + wo;
    if (cosTheta_i == 0 || cosTheta_o == 0 || LengthSquared(wm) == 0) return {};
    // 这里传入的wm是Normalize的
    wm = FaceForward(Normalize(wm), Normal3f(0, 0, 1));

FaceForward定义如下

template <typename T>
Normal3<T> FaceForward(Normal3<T> n, Vector3<T> v) {
    return (Dot(n, v) < 0.f) ? -n : n;
}

保证wm的方向与宏观法线在同一侧。
计算出wm后,和之前一样,需要保证wm与宏观表面的法线是在同一侧的,因为需要忽略背向的微平面。

    if (Dot(wm, wi) * cosTheta_i < 0 || Dot(wm, wo) * cosTheta_o < 0)
        return {};

通过保证wi(或wo)与wm的夹角cos值与wi(或wo)与宏观法线的夹角cos值同号来保证wm与宏观法线方向一致。
后续反射和透射的BSDF值计算与Sample_f相同。

pdf

Float DielectricBxDF::PDF(Vector3f wo, Vector3f wi, TransportMode mode,
          BxDFReflTransFlags sampleFlags) const {
    // 镜面反射依然返回0
    if (eta == 1 || mfDistrib.EffectivelySmooth())
        return 0;
 
    // 计算微平面法线,并以此计算菲涅尔项
    Float cosTheta_o = CosTheta(wo), cosTheta_i = CosTheta(wi);
    bool reflect = cosTheta_i * cosTheta_o > 0;
    float etap = 1;
    if (!reflect)
        etap = cosTheta_o > 0 ? eta : (1 / eta);
    Vector3f wm = wi * etap + wo;
    if (cosTheta_i == 0 || cosTheta_o == 0 || LengthSquared(wm) == 0) return {};
    wm = FaceForward(Normalize(wm), Normal3f(0, 0, 1));
 
    if (Dot(wm, wi) * cosTheta_i < 0 || Dot(wm, wo) * cosTheta_o < 0)
        return {};
 
    Float R = FrDielectric(Dot(wo, wm), eta);
    Float T = 1 - R;
 
    Float pr = R, pt = T;
    if (!(sampleFlags & BxDFReflTransFlags::Reflection)) pr = 0;
    if (!(sampleFlags & BxDFReflTransFlags::Transmission)) pt = 0;
    if (pr == 0 && pt == 0)
        return {};
 
    Float pdf;
    if (reflect) {
        pdf = mfDistrib.PDF(wo, wm) / (4 * AbsDot(wo, wm)) * pr / (pr + pt);
    } else {
        Float denom = Sqr(Dot(wi, wm) + Dot(wo, wm) / etap);
        Float dwm_dwi = AbsDot(wi, wm) / denom;
        pdf = mfDistrib.PDF(wo, wm) * dwm_dwi * pt / (pr + pt);
    }
    return pdf;
}

pdf的计算方式在上面已经介绍过了,没有新的概念,不再赘述。

薄绝缘体BSDF

薄的绝缘体和普通绝缘体的反射模型有着较大差异,由于物体较薄,透射的光线会在物体内部反射后回到物体表面,这就导致反射的能量比菲涅尔方程计算出的要大。下面是一个薄绝缘体反射模型的示意图。

光线在物体内部不断发生弹射,导致反射出的光线增多。
最终反射的光线能量计算,

根据能量守恒定律,透射光线的能量为

它的BSDF定义:

class ThinDielectricBxDF {
  public:
    ...
  private:
    Float eta;
};

薄的绝缘体不再考虑粗糙度和微平面,假设它们都是光滑的镜面(比如玻璃片、塑料袋),所以成员变量中只有折射率eta。

Sample_f

pstd::optional<BSDFSample>
Sample_f(Vector3f wo, Float uc, Point2f u, TransportMode mode,
         BxDFReflTransFlags sampleFlags) const {
    Float R = FrDielectric(AbsCosTheta(wo), eta), T = 1 - R;
    if (R < 1) {
        R += Sqr(T) * R / (1 - Sqr(R));
        T = 1 - R;
    }
 
    Float pr = R, pt = T;
    if (!(sampleFlags & BxDFReflTransFlags::Reflection)) pr = 0;
    if (!(sampleFlags & BxDFReflTransFlags::Transmission)) pt = 0;
    if (pr == 0 && pt == 0)
        return {};
 
    if (uc < pr / (pr + pt)) {
        Vector3f wi(-wo.x, -wo.y, wo.z);
        SampledSpectrum fr(R / AbsCosTheta(wi));
        return BSDFSample(fr, wi, pr / (pr + pt), BxDFFlags::SpecularReflection);
    } else {
        Vector3f wi = -wo;
        SampledSpectrum ft(T / AbsCosTheta(wi));
        return BSDFSample(ft, wi, pt / (pr + pt), BxDFFlags::SpecularTransmission);
    }
}

增加了一个步骤,使用FrDielectric计算出R和T之后,如果R<1,也就是会同时发生反射和透射,那么其R和T需要上面的公式重新计算,

    if (R < 1) {
        R += Sqr(T) * R / (1 - Sqr(R));
        T = 1 - R;
    }

还有一个需要注意的是透射光方向的计算,

    Vector3f wi = -wo;

透射光方向为入射光方向的反方向,这个从上面的示意图中可以看出,由于发生了两次折射,光线的方向并没有发生变化,所以可以直接取入射光的反方向。严格来说,透射出的光线和入射光方向是有一定偏移的,这里做了简化,忽略了这种偏移。
这里取反的原因是,在程序中,为了方便计算,所有的光线方向定义都是从物体出发,比如物体到光源,物体到相机。

f

由于是完美的镜面反射,BSDF值返回0,上面f章节有解释。

       SampledSpectrum f(Vector3f wo, Vector3f wi, TransportMode mode) const {
           return SampledSpectrum(0);
       }

pdf

镜面反射,pdf函数直接返回0,

       Float PDF(Vector3f wo, Vector3f wi, TransportMode mode,
                 BxDFReflTransFlags sampleFlags) const {
           return 0;
       }