PBRT读书笔记09-Light Sources

 

这个系列是Physically Based Rendering: From Theory To Implementation的读书笔记,本节主要介绍PBRT中的光源。

为了渲染图像我们需要在场景中添加光源。PBRT中提供了不同类型光源的实现,本节会介绍常用光源中比较简单的几种类型。

Light Emission

任何温度高于绝对零度的物体都包含运动的原子。根据Maxwell方程组,运动的原子会在一定的波长范围内向外发射电磁波。对于室温环境中的物体,其辐射大部分位于红外辐射的频率范围内;而如果想要达到可见光的范围则需要高得多的温度。

根据发光的物理过程,日常生活中常见的光源可以分为以下几种:

  • 白炽灯(incandescent (tungsten) lamps):通过电流加热灯丝实现发光,光线的频率分布与灯丝的温度有关。在发光过程中大部分的电能转换为了热能而不是光。
  • 卤素灯(halogen lamps):发光原理与白炽灯类似,但在灯泡中加入了卤素气体来延长灯丝的寿命。
  • 气体放电灯(gas-discharge lamps):在灯泡中充入气体各种气体和金属气体,通电后利用原子放电实现发光。
  • LED灯:LED灯基于电致发光(electroluminescent)原理,当材料通电后会激发电子跃迁导致发光。

以上几种发光过程的本质都是电子和原子发生碰撞后外层电子会发生层级跃迁现象,当电子返回低能级时则会发射光子将能量释放。除了上面的几种发光过程,常见的发光过程还包括化学发光(chemoluminescence)和生物发光(bioluminescence)等。

我们定义光视效能(luminous efficacy)为光通量与辐射通量的比值,它表示光源将能量转换为可见光的效率:

\[\frac{\int \Phi_e(\lambda) V(\lambda) \ d \lambda}{\int \Phi_i(\lambda) \ d \lambda}\]

其中$V(\lambda)$为观测者在不同波长$\lambda$下的响应曲线。光视效能的单位是Watt,如果把分母部分定义为光源消耗的能量,则光视效能也描述了光源将能量转换为电磁辐射的效率。

Blackbody Emitters

黑体(blackbody)是理想的辐射体,它会将能量尽可能高效地转换为电磁辐射。黑体的另一个特点是它会吸收所有的能量而不会发生任何反射行为,因此任何光照下黑体都是完美的黑色。普朗克定律(Planck’s law)给出了黑体辐射与波长$\lambda$以及温度$T$之间的关系:

\[L_e(\lambda, T) = \frac{2 h c^2}{\lambda^5 (e^{\frac{hc}{\lambda k_b T}} - 1)}\]

其中$c$为光速,$h$为Planck常数,$k_b$为Boltzmann常数。不同温度下黑体在不同波长上的辐射曲线可参考下图:

在PBRT中使用Blackbody()函数计算黑体在指定温度和波长下的radiance:

void Blackbody(const Float *lambda, int n, Float T, Float *Le) {
    const Float c = 299792458;
    const Float h = 6.62606957e-34;
    const Float kb= 1.3806488e-23;

    for (int i = 0; i < n; ++i) {
        // Compute emitted radiance for blackbody at wavelength lambda[i]
        Float l = lambda[i] * 1e-9;
        Float lambda5 = (l * l) * (l * l) * l;
        Le[i] = (2 * h * c * c) /
                (lambda5 * (std::exp((h * c) / (l * kb  * T)) - 1));
    }
}

根据Stefan–Boltzmann定律(Stefan–Boltzmann law),黑体上任意一点$p$处的全频段辐出功率为:

\[M(p) = \sigma T^4\]

其中$\sigma$为Stefan–Boltzmann常数。

在很多情况下我们需要对黑体辐射进行规范化使得规范化后SPD的最大值为1,此时则需要使用维恩位移定律(Wien’s displacement law)来计算黑体辐射在温度$T$下的峰值波长:

\[\lambda_\text{max} = \frac{b}{T}\]

其中$b$为维恩位移常数。在PBRT中使用BlackbodyNormalized()函数来计算规范化后的SPD:

void BlackbodyNormalized(const Float *lambda, int n, Float T, Float *Le) {
    Blackbody(lambda, n, T, Le);

    // Normalize Le values based on maximum blackbody radiance
    Float lambdaMax = 2.8977721e-3 / T * 1e9;
    Float maxL;
    Blackbody(&lambdaMax, 1, T, &maxL);

    for (int i = 0; i < n; ++i)
        Le[i] /= maxL;
}

对于非黑体,其发射行为由Kirchhoff定律(Kirchhoff’s law)给出。Kirchhoff定律说明对于达到热平衡的物体,它在任意温度和频率上发射的radiance等于理想黑体在同样状态下发射的radiance乘以物体吸收能量与入射能量的比例。由于物体吸收能量的比例与反射能量的比例之和为1,Kirchhoff定律可以表示为:

\[L'_e(T, \omega, \lambda) = L_e(T, \lambda) \ (1 - \rho_\text{hd}(\omega))\]

其中$\rho_\text{hd}(\omega)$为定向半球反射率(hemispherical-directional reflectance)

\[\rho_\text{hd} (\omega_o) = \int_{H^2(\mathbf{n})} f_r(p, \omega_o, \omega_i) \vert \cos \theta_i \vert \ d \omega_i\]

利用黑体辐射的SPD我们还可以定义色温(color temperature)。如果物体辐射的SPD与黑体在某一温度$T$下的SPD类似,则称物体的色温为$T$。在计算色温时的一种方法是首先计算光源发射SPD取最大时的波长,然后利用维恩位移定律计算出该波长对应黑体辐射的温度。

对于日常生活中常见的光源,白炽灯的色温一般在2,700K附近,卤素灯的色温在3,000K附近,而荧光灯的色温则在2,700-6,500K范围上。通常情况下我们称色温高于5,000K的为冷色光,2,700–3,000K的为暖色光。

Standard Illuminants

描述光源的另一种方法是使用CIE规定的标准光源(standard illuminants)。其中A类光源的SPD如下图所示,它相当于理想黑体2,856K温度下的辐射,足以表示大部分白炽灯的行为。

B类和C类光源用来表示一天中两个时间段的日照光,现已弃用。D类光源用来描述日照光的变化,它由一个固定项和两个可变项组成。两个可变项分别为云对应的黄-蓝颜色变化以及大气水分对应的粉-绿颜色变化。D65光源的SPD曲线可见下图,它的色温约为6,504K对应欧洲正午时的日照。

F型光源对应荧光,它们的曲线来自于对真实荧光光源的测量。

Light Interface

PBRT中使用Light类作为各种光源的通用接口,在进行初始化时需要提供4个参数:

  • flag参数表示基础光源的类型,在积分时积分器会根据flag参数调整计算方法。
  • LightToWorld参数定义了光源到世界坐标系的坐标变换。
  • mediumInterface参数定义了光源位于的介质。
  • nSamples参数定义了对光源进行采样时的样本数量。

Light类中比较重要的成员函数是Light::Sample_Li(),它用来计算没有遮挡情况下光源发出的光线到达$p$点时的radiance。对于一部分光源可能存在不止一条到达指定的的光路,此时Light::Sample_Li()函数会先在光源上随机采样然后再进行计算。

Light::Power()函数用来计算光源发出的总能量,这在某些光线传输算法中有着重要的作用。此外Light类还包含一个Light::Preprocess()函数用来对场景进行预处理。

Light类的完整定义如下:

enum class LightFlags : int {
    DeltaPosition = 1, DeltaDirection = 2, Area = 4, Infinite = 8
};

inline bool IsDeltaLight(int flags) {
    return flags & (int)LightFlags::DeltaPosition ||
           flags & (int)LightFlags::DeltaDirection;
}

class Light {
public:
    virtual ~Light();
    Light(int flags, const Transform &LightToWorld,
          const MediumInterface &mediumInterface, int nSamples = 1)
        : flags(flags), nSamples(std::max(1, nSamples)),
          mediumInterface(mediumInterface), LightToWorld(LightToWorld),
          WorldToLight(Inverse(LightToWorld))  { 
        // Warn if light has transformation with non-uniform scale>
        if (WorldToLight.HasScale())
            Warning("Scaling detected in world to light transformation!\n"
                    "The system has numerous assumptions, implicit and explicit,\n"
                    "that this transform will have no scale factors in it.\n"
                    "Proceed at your own risk; your image may have errors or\n"
                    "the system may crash as a result of this.");
       }

    virtual Spectrum Sample_Li(const Interaction &ref, const Point2f &u, 
                     		   Vector3f *wi, Float *pdf, VisibilityTester *vis) const = 0;
    virtual Spectrum Power() const = 0;
    virtual void Preprocess(const Scene &scene) { }
    virtual Spectrum Le(const RayDifferential &r) const;
    virtual Float Pdf_Li(const Interaction &ref,  const Vector3f &wi) const = 0;
    virtual Spectrum Sample_Le(const Point2f &u1, const Point2f &u2,
                               Float time, Ray *ray, Normal3f *nLight,
                               Float *pdfPos, Float *pdfDir) const = 0;
    virtual void Pdf_Le(const Ray &ray, const Normal3f &nLight,
                        Float *pdfPos, Float *pdfDir) const = 0;

public:
    const int flags;
    const int nSamples;
    const MediumInterface mediumInterface;

protected:
    const Transform LightToWorld, WorldToLight;
};

Spectrum Light::Le(const RayDifferential &ray) const {
    return Spectrum(0.f);
}

Visibility Testing

VisibilityTester类用来测试两个点之间是否存在遮挡,它的基本定义如下:

class VisibilityTester {
public:
    VisibilityTester(const Interaction &p0, const Interaction &p1)
        : p0(p0), p1(p1) { }
    const Interaction &P0() const { return p0; }
    const Interaction &P1() const { return p1; }
    bool Unoccluded(const Scene &scene) const;
    Spectrum Tr(const Scene &scene, Sampler &sampler) const;

private:
    Interaction p0, p1;
};

VisibilityTester类包含两种方法来判断可见性。首先是VisibilityTester::Unoccluded()函数,它会从p0p1发射光线并利用场景是否与这条光线相交来判断可见性。

bool VisibilityTester::Unoccluded(const Scene &scene) const {
    return !scene.IntersectP(p0.SpawnRayTo(p1));
}

如果需要考虑介质的散射行为则可以使用VisibilityTester::Tr()函数,它会考虑光线的透射率并返回光线衰减后剩余的比例。

Spectrum VisibilityTester::Tr(const Scene &scene,
                              Sampler &sampler) const {
    Ray ray(p0.SpawnRayTo(p1));
    Spectrum Tr(1.f);
    while (true) {
        SurfaceInteraction isect;
        bool hitSurface = scene.Intersect(ray, &isect);
        // Handle opaque surface along ray’s path
        if (hitSurface && isect.primitive->GetMaterial() != nullptr)
            return Spectrum(0.0f);

        // Update transmittance for current ray segment
        if (ray.medium)
            Tr *= ray.medium->Tr(ray, sampler);

        // Generate next ray segment or return final transmittance
        if (!hitSurface)
            break;
        ray = isect.SpawnRayTo(p1);
    }

    return Tr;
}

Point Lights

最基础的光源类型是点光源,在PBRT中使用PointLight进行描述。在初始化时点光源的flag成员变量会直接初始化为LightFlags::DeltaPosition;同时对点光源进行采样时也无需真的进行采样,直接计算接收点获得的radiance即可。

class PointLight : public Light {
public:
    PointLight(const Transform &LightToWorld,
                  const MediumInterface &mediumInterface, const Spectrum &I)
           : Light((int)LightFlags::DeltaPosition, LightToWorld,
                   mediumInterface),
             pLight(LightToWorld(Point3f(0, 0, 0))), I(I) { }

    Spectrum Sample_Li(const Interaction &ref, const Point2f &u, 
                       Vector3f *wi, Float *pdf, VisibilityTester *vis) const;
    Spectrum Power() const;
    Float Pdf_Li(const Interaction &, const Vector3f &) const;
    Spectrum Sample_Le(const Point2f &u1, const Point2f &u2,
                       Float time, Ray *ray, Normal3f *nLight, Float *pdfPos, Float *pdfDir) const;
    void Pdf_Le(const Ray &, const Normal3f &, Float *pdfPos, Float *pdfDir) const;

private:
    const Point3f pLight;
    const Spectrum I;
};

Spectrum PointLight::Sample_Li(const Interaction &ref,
        const Point2f &u, Vector3f *wi, Float *pdf,
        VisibilityTester *vis) const {
    *wi = Normalize(pLight - ref.p);
    *pdf = 1.f;
    *vis = VisibilityTester(ref, Interaction(pLight, ref.time,
                                             mediumInterface));
    return I / DistanceSquared(pLight, ref.p);
}

Spectrum PointLight::Power() const {
    return 4 * Pi * I;
}

Spotlights

点光源的一个常见变体是聚光灯,此时光线会分布在一些固定的方向上而不会项四面八方扩散。在PBRT中使用SpotLight来表示这类光源:

class SpotLight : public Light {
public:
    SpotLight(const Transform &LightToWorld, const MediumInterface &m, const Spectrum &I,
              Float totalWidth, Float falloffStart);
    Spectrum Sample_Li(const Interaction &ref, const Point2f &u,
                       Vector3f *wi, Float *pdf, VisibilityTester *vis) const;
    Float Falloff(const Vector3f &w) const;
    Spectrum Power() const;
    Float Pdf_Li(const Interaction &, const Vector3f &) const;
    Spectrum Sample_Le(const Point2f &u1, const Point2f &u2,
                       Float time, Ray *ray, Normal3f *nLight, Float *pdfPos,
                       Float *pdfDir) const;
    void Pdf_Le(const Ray &, const Normal3f &, Float *pdfPos, Float *pdfDir) const;

private:
    const Point3f pLight;
    const Spectrum I;
    const Float cosTotalWidth, cosFalloffStart;
};

初始化SpotLight时需要提供两个额外的参数falloffStarttotalWidth。当物体与光源的夹角小于falloffStart时会接收完整的光照,当物体与光源的夹角大于totalWidth则不会接收到光照,如果夹角位于二者之间则会产生渐变的效果。为了便于计算,SpotLight会在初始化时直接保存两个角度的余弦。

SpotLight::SpotLight(const Transform &LightToWorld,
        const MediumInterface &mediumInterface, const Spectrum &I,
        Float totalWidth, Float falloffStart)
    : Light((int)LightFlags::DeltaPosition, LightToWorld, mediumInterface),
      pLight(LightToWorld(Point3f(0, 0, 0))), I(I),
      cosTotalWidth(std::cos(Radians(totalWidth))),
      cosFalloffStart(std::cos(Radians(falloffStart))) { }

SpotLight对光源采样的方式与PointLight基本一致,唯一的区别在于当光线夹角位于falloffStarttotalWidth之间时需要考虑光照的渐变效果。SpotLight使用SpotLight::Falloff()方法来计算光照的衰减,这里通过夹角余弦进行插值来计算衰减系数。

Spectrum SpotLight::Sample_Li(const Interaction &ref,
        const Point2f &u, Vector3f *wi, Float *pdf,
        VisibilityTester *vis) const {
    *wi = Normalize(pLight - ref.p);
    *pdf = 1.f;
    *vis = VisibilityTester(ref, Interaction(pLight, ref.time,
                                             mediumInterface));
    return I * Falloff(-*wi) / DistanceSquared(pLight, ref.p);
}

Float SpotLight::Falloff(const Vector3f &w) const {
    Vector3f wl = Normalize(WorldToLight(w));
    Float cosTheta = wl.z;
    if (cosTheta < cosTotalWidth)     return 0;
    if (cosTheta > cosFalloffStart)   return 1;

    // Compute falloff inside spotlight cone
    Float delta = (cosTheta - cosTotalWidth) /
                  (cosFalloffStart - cosTotalWidth);
    return (delta * delta) * (delta * delta);
}

聚光灯的能量由SpotLight::Power()方法计算,计算时同样只需要考虑totalWidth范围内的光照。

Spectrum SpotLight::Power() const {
    return I * 2 * Pi * (1 - .5f * (cosFalloffStart + cosTotalWidth));
}

Texture Projection Lights

我们还可以把纹理作为光源投影到场景中,这样的光源定义为ProjectionLight

class ProjectionLight : public Light {
public:
    ProjectionLight(const Transform &LightToWorld, const MediumInterface &medium,
                    const Spectrum &I, const std::string &texname, Float fov);

    Spectrum Sample_Li(const Interaction &ref, const Point2f &u,
           Vector3f *wi, Float *pdf, VisibilityTester *vis) const;
    Spectrum Projection(const Vector3f &w) const;
    Spectrum Power() const;
    Float Pdf_Li(const Interaction &, const Vector3f &) const;
    Spectrum Sample_Le(const Point2f &u1, const Point2f &u2,
                       Float time, Ray *ray, Normal3f *nLight, Float *pdfPos, Float *pdfDir) const;
    void Pdf_Le(const Ray &, const Normal3f &, Float *pdfPos, Float *pdfDir) const;

private:
    std::unique_ptr<MIPMap<RGBSpectrum>> projectionMap;
    const Point3f pLight;
    const Spectrum I;
    Transform lightProjection;
    Float near, far;
    Bounds2f screenBounds;
    Float cosTotalWidth;
};

ProjectionLight在初始化时会同时载入一个纹理对象,并且利用投影函数计算光源作用的范围。

ProjectionLight::ProjectionLight(const Transform &LightToWorld,
        const MediumInterface &mediumInterface, const Spectrum &I,
        const std::string &texname, Float fov)
    : Light((int)LightFlags::DeltaPosition, LightToWorld, mediumInterface),
      pLight(LightToWorld(Point3f(0, 0, 0))), I(I) {
    // Create ProjectionLight MIP map
    Point2i resolution;
    std::unique_ptr<RGBSpectrum[]> texels = ReadImage(texname, &resolution);
    if (texels)
        projectionMap.reset(new MIPMap<RGBSpectrum>(resolution, texels.get()));

    // Initialize ProjectionLight projection matrix
    Float aspect = projectionMap ?
                   (Float(resolution.x) / Float(resolution.y)) : 1;
    if (aspect > 1)
        screenBounds = Bounds2f(Point2f(-aspect, -1), Point2f(aspect, 1));
    else
        screenBounds = Bounds2f(Point2f(-1, -1/aspect), Point2f(1, 1/aspect));
    
    near = 1e-3f;
    far = 1e30f;
    lightProjection = Perspective(fov, near, far);

    // Compute cosine of cone surrounding projection directions
    Float opposite = std::tan(Radians(fov) / 2.f);
    Float tanDiag = opposite * std::sqrt(1 + 1 / (aspect * aspect));
    cosTotalWidth = std::cos(std::atan(tanDiag));
}

类似于SpotLightProjectionLight在进行采样时也定义了一个辅助函数ProjectionLight::Projection()用来计算纹理投影的效果。

Spectrum ProjectionLight::Projection(const Vector3f &w) const {
    Vector3f wl = WorldToLight(w);
    // Discard directions behind projection light
    if (wl.z < near) return 0;

    // Project point onto projection plane and compute light
    Point3f p = lightProjection(Point3f(wl.x, wl.y, wl.z));
    if (!Inside(Point2f(p.x, p.y), screenBounds)) return 0.f;
    if (!projectionMap) return 1;
    Point2f st = Point2f(screenBounds.Offset(Point2f(p.x, p.y)));
    return Spectrum(projectionMap->Lookup(st), SpectrumType::Illuminant);
}

SpotLight的能量由ProjectionLight::Power()方法进行计算,需要注意这是一种粗略的一阶近似:

Spectrum ProjectionLight::Power() const {
    return (projectionMap ?
            Spectrum(projectionMap->Lookup(Point2f(.5f, .5f), .5f), SpectrumType::Illuminant) : 
            Spectrum(1.f)) * I * 2 * Pi * (1.f - cosTotalWidth);
}

Goniophotometric Diagram Lights

在照明工程中经常使用测光表(goniophotometric diagram)来描述来自光源的光线随角度的变化,在PBRT中使用GonioPhotometricLight来描述这样定义的光源。GonioPhotometricLightProjectionLight非常类似都包含一个纹理图像,但不同的是GonioPhotometricLight的纹理图像使用了球坐标进行参数化,因此在进行查询时需要使用GonioPhotometricLight::Scale()方法来转换到参数平面上。

class GonioPhotometricLight : public Light {
public:
    Spectrum Sample_Li(const Interaction &ref, const Point2f &u,
                       Vector3f *wi, Float *pdf, VisibilityTester *vis) const;
    
    GonioPhotometricLight(const Transform &LightToWorld,
                          const MediumInterface &mediumInterface, const Spectrum &I,
                          const std::string &texname)
           : Light((int)LightFlags::DeltaPosition, LightToWorld, mediumInterface),
             pLight(LightToWorld(Point3f(0, 0, 0))), I(I) {
        // Create mipmap for GonioPhotometricLight
        Point2i resolution;
        std::unique_ptr<RGBSpectrum[]> texels = ReadImage(texname, &resolution);
        if (texels)
            mipmap.reset(new MIPMap<RGBSpectrum>(resolution, texels.get()));
    }

    Spectrum Scale(const Vector3f &w) const {
        Vector3f wp = Normalize(WorldToLight(w));
        std::swap(wp.y, wp.z);
        Float theta = SphericalTheta(wp);
        Float phi   = SphericalPhi(wp);
        Point2f st(phi * Inv2Pi, theta * InvPi);
        return !mipmap ? RGBSpectrum(1.f) :
               Spectrum(mipmap->Lookup(st), SpectrumType::Illuminant);
    }

    Spectrum Power() const;
    Float Pdf_Li(const Interaction &, const Vector3f &) const;
    Spectrum Sample_Le(const Point2f &u1, const Point2f &u2,
                       Float time, Ray *ray, Normal3f *nLight, Float *pdfPos, Float *pdfDir) const;
    void Pdf_Le(const Ray &, const Normal3f &, Float *pdfPos, Float *pdfDir) const;

private:
    const Point3f pLight;
    const Spectrum I;
    std::unique_ptr<MIPMap<RGBSpectrum>> mipmap;
};

Spectrum GonioPhotometricLight::Power() const {
    return 4 * Pi * I *
        Spectrum(mipmap ? mipmap->Lookup(Point2f(.5f, .5f), .5f) :
                 Spectrum(1.f), SpectrumType::Illuminant);
}

Distant Lights

第二种常见的光源形式是平行光源,它可以理解为点光源与场景距离无穷远时的极限情况,在这种情况下可以认为场景接收到的光源都是平行的。在PBRT中使用DistantLight来描述这种类型的光源:

class DistantLight : public Light {
public:
    DistantLight(const Transform &LightToWorld, const Spectrum &L, const Vector3f &w);
    
    void Preprocess(const Scene &scene) {
        scene.WorldBound().BoundingSphere(&worldCenter, &worldRadius);
    }

    Spectrum Sample_Li(const Interaction &ref, const Point2f &u,
                       Vector3f *wi, Float *pdf, VisibilityTester *vis) const;
    Spectrum Power() const;
    Float Pdf_Li(const Interaction &, const Vector3f &) const;
    Spectrum Sample_Le(const Point2f &u1, const Point2f &u2,
                       Float time, Ray *ray, Normal3f *nLight, Float *pdfPos, Float *pdfDir) const;
    void Pdf_Le(const Ray &, const Normal3f &, Float *pdfPos, Float *pdfDir) const;

private:
    const Spectrum L;
    const Vector3f wLight;
    Point3f worldCenter;
    Float worldRadius;
};

注意在初始化DistantLight时会直接将介质设置为空,这是因为无穷远距离的光源穿过介质后一定会被完全吸收,因此唯一可能出现无穷远光源的情况是真空。

DistantLight::DistantLight(const Transform &LightToWorld, 
        const Spectrum &L, const Vector3f &wLight)
    : Light((int)LightFlags::DeltaDirection, LightToWorld,
            MediumInterface()),
      L(L), wLight(Normalize(LightToWorld(wLight))) { }

DistantLight进行采样比较简单,不过在使用VisibilityTester时目标点为以$p$为起点按wLight方向前进两倍worldRadius距离处的点。

Spectrum DistantLight::Sample_Li(const Interaction &ref,
        const Point2f &u, Vector3f *wi, Float *pdf,
        VisibilityTester *vis) const {
    *wi = wLight; 
    *pdf = 1;
    Point3f pOutside = ref.p + wLight * (2 * worldRadius);
    *vis = VisibilityTester(ref, Interaction(pOutside, ref.time,
                                             mediumInterface));
    return L;
}

DistantLight的能量定义为整个场景圆盘上接收到的能量:

Spectrum DistantLight::Power() const {
    return L * Pi * worldRadius * worldRadius;
}

Area Lights

第三种常见的光源是面光源,它表示由几何体构成的光源。处理这种光源时往往需要使用Monte Carlo积分并对发光体表面进行采样,具体的计算方法会在后面的章节进行介绍。

在PBRT中使用AreaLight作为面光源的通用接口,所有具体的面光源实现都要继承这个类。其中AreaLight::L()方法用来计算光源上的点在给定方向上发出的radiance,这个方法会被SurfaceInteraction对象的SurfaceInteraction::Le()调用以获得发射的能量。

class AreaLight : public Light {
public:
    AreaLight(const Transform &LightToWorld, const MediumInterface &medium, int nSamples)
        : Light((int)LightFlags::Area, LightToWorld, medium, nSamples) { }
    
    virtual Spectrum L(const Interaction &intr, const Vector3f &w) const = 0;
};

Spectrum SurfaceInteraction::Le(const Vector3f &w) const {
    const AreaLight *area = primitive->GetAreaLight();
    return area ? area->L(*this, w) : Spectrum(0.f);
}

DiffuseAreaLight类表示均匀发射光线的面光源。发光表面由shape成员变量定义,而且它只会向法向所在的半球面上发光。DiffuseAreaLight的定义如下:

class DiffuseAreaLight : public AreaLight {
public:
    DiffuseAreaLight(const Transform &LightToWorld, const MediumInterface &mediumInterface,
                     const Spectrum &Le, int nSamples,
                     const std::shared_ptr<Shape> &shape);

    Spectrum L(const Interaction &intr, const Vector3f &w) const {
        return Dot(intr.n, w) > 0.f ? Lemit : Spectrum(0.f);
    }

    Spectrum Power() const;
    Spectrum Sample_Li(const Interaction &ref, const Point2f &u, Vector3f *wo,
                       Float *pdf, VisibilityTester *vis) const;
    Float Pdf_Li(const Interaction &, const Vector3f &) const;
    Spectrum Sample_Le(const Point2f &u1, const Point2f &u2,
                       Float time, Ray *ray, Normal3f *nLight, Float *pdfPos, Float *pdfDir) const;
    void Pdf_Le(const Ray &, const Normal3f &, Float *pdfPos, Float *pdfDir) const;

protected:
    const Spectrum Lemit;
    std::shared_ptr<Shape> shape;
    const Float area;
};

DiffuseAreaLight::DiffuseAreaLight(const Transform &LightToWorld,
        const MediumInterface &mediumInterface, const Spectrum &Lemit,
        int nSamples, const std::shared_ptr<Shape> &shape)
    : AreaLight(LightToWorld, mediumInterface, nSamples), Lemit(Lemit),
      shape(shape), area(shape->Area()) { }

由于只考虑法向半球上出射的光线,在计算DiffuseAreaLight::L()时需要考虑光线方向和法向的夹角,如果在负半球上则直接返回0。而计算接收来自DiffuseAreaLight的radiance时需要对光源表面进行采样而无法像前面介绍过的光源那样直接进行计算,具体的计算方法比较复杂我们留到后面的章节再来介绍。

DiffuseAreaLight发射的总能量可以通过公式来直接计算:

Spectrum DiffuseAreaLight::Power() const {
    return Lemit * area * Pi;
}

Infinite Area Lights

最后一种常用的光源是无限面光源,它一般用来表示来自真实环境的光照。在PBRT中使用InfiniteAreaLight来表示无限面光源:

class InfiniteAreaLight : public Light {
public:
    InfiniteAreaLight(const Transform &LightToWorld, const Spectrum &power,
                         int nSamples, const std::string &texmap);
    
    void Preprocess(const Scene &scene) {
        scene.WorldBound().BoundingSphere(&worldCenter, &worldRadius);
    }
    
    Spectrum Power() const;
    Spectrum Le(const RayDifferential &ray) const;
    Spectrum Sample_Li(const Interaction &ref, const Point2f &u,
           Vector3f *wi, Float *pdf, VisibilityTester *vis) const;
    Float Pdf_Li(const Interaction &, const Vector3f &) const;
    Spectrum Sample_Le(const Point2f &u1, const Point2f &u2,
                       Float time, Ray *ray, Normal3f *nLight, Float *pdfPos, Float *pdfDir) const;
    void Pdf_Le(const Ray &, const Normal3f &, Float *pdfPos, Float *pdfDir) const;

private:
    std::unique_ptr<MIPMap<RGBSpectrum>> Lmap;
    Point3f worldCenter;
    Float worldRadius;
    std::unique_ptr<Distribution2D> distribution;
};

InfiniteAreaLight在进行初始化时需要读取一张环境光图像表示来自不同方向的光照,同时也需要把参与介质设置为空。

InfiniteAreaLight::InfiniteAreaLight(const Transform &LightToWorld,
        const Spectrum &L, int nSamples, const std::string &texmap)
    : Light((int)LightFlags::Infinite, LightToWorld,
      MediumInterface(), nSamples) {
    // Read texel data from texmap and initialize Lmap
    Point2i resolution;
    std::unique_ptr<RGBSpectrum[]> texels(nullptr);
    if (texmap != "") {
        texels = ReadImage(texmap, &resolution);
        if (texels)
            for (int i = 0; i < resolution.x * resolution.y; ++i)
                texels[i] *= L.ToRGBSpectrum();
    }

    if (!texels) {
        resolution.x = resolution.y = 1;
        texels = std::unique_ptr<RGBSpectrum[]>(new RGBSpectrum[1]);
        texels[0] = L.ToRGBSpectrum();
    }

    Lmap.reset(new MIPMap<RGBSpectrum>(resolution, texels.get()));

    // Initialize sampling PDFs for infinite area light
    // Compute scalar-valued image img from environment map
    int width = resolution.x, height = resolution.y;
    Float filter = (Float)1 / std::max(width, height);
    std::unique_ptr<Float[]> img(new Float[width * height]);
    for (int v = 0; v < height; ++v) {
        Float vp = (Float)v / (Float)height;
        Float sinTheta = std::sin(Pi * Float(v + .5f) / Float(height));

        for (int u = 0; u < width; ++u) {
            Float up = (Float)u / (Float)width;
            img[u + v * width] = Lmap->Lookup(Point2f(up, vp), filter).y();
            img[u + v * width] *= sinTheta;
        }
    }

    // Compute sampling distributions for rows and columns of image
    distribution.reset(new Distribution2D(img.get(), width, height));
}

由于InfiniteAreaLight需要考虑所有方向发射的光线,调用InfiniteAreaLight::Sample_Li()时需要采样来估计接收到的radiance。具体的计算方法同样放到后面的章节再进行介绍。

类似于平行光,InfiniteAreaLight在计算光照总能量时也使用了场景圆盘接收到的能量进行近似。

Spectrum InfiniteAreaLight::Power() const {
    return Pi * worldRadius * worldRadius *
        Spectrum(Lmap->Lookup(Point2f(.5f, .5f), .5f),
                 SpectrumType::Illuminant);
}

和前面介绍的其它光源不同的是,InfiniteAreaLight需要考虑那些没有和场景发生相交的光线。因此它需要重写InfiniteAreaLight::Le()方法来考虑这种类型的radiance。

Spectrum InfiniteAreaLight::Le(const RayDifferential &ray) const {
    Vector3f w = Normalize(WorldToLight(ray.d));
    Point2f st(SphericalPhi(w) * Inv2Pi,
               SphericalTheta(w) * InvPi);
    return Spectrum(Lmap->Lookup(st), SpectrumType::Illuminant);
}

Reference