BlueRose
文章97
标签28
分类7
PBRT笔记(11)——光源

PBRT笔记(11)——光源

自发光灯光

至今为止,人们发明了很多光源,现在被广泛使用的有:

  1. 白炽灯的钨丝很小。电流通过灯丝时,使得灯丝升温,从而使灯丝发出电磁波,其波长的分布取决于灯丝的温度。但大部分能量都被转化为热能而不是光能。
  2. 卤素灯,在灯中充入惰性气体,使得灯的寿命增加,与白炽灯一样使用钨丝。
  3. LED灯

发光效率是衡量光源将能量转化为可见光的效率。对人眼来说,非可见光波长的辐射几乎没有价值,有趣的是,它是光度量(发出的光通量)与辐射量(它所使用的总功率或它发出的总波长的总功率(以光通量计算)之比:

其中V(λ)为光谱响应曲线。
发光效率的单位是流明/瓦特,如果i是光源消耗的功率(而不是发射的功率),那么发光效率也包含了光源将功率转换成电磁辐射的有效程度的度量。发光效率也可以定义为表面某一特定方向上的某一点上的发光强度与辐照度的比值(辐射强度的光度当量),也可以定义为表面某一特定方向上的某一点上的发光强度与辐照度的比值。

黑体发射器

黑体,是一个理想化了的物体,它能有效地将能量转化为电磁辐射。虽然真正的黑体在物理上是不可实现的,但一些发射器表现出接近黑体的行为。黑体也有一个有用的封闭形式的表达式,作为温度和波长的函数,这是对于黑体发射器来说非常有用的建模。

黑体之所以如此命名,是因为它们吸收了所有的入射能量,并且没有反射。因此,一个真正的黑体看起来是完全黑色的。它能够吸收外来的全部电磁辐射,并且不会有任何的反射与透射。换句话说,黑体对于任何波长的电磁波的吸收系数为1,透射系数为0。

普朗克定律给发出一个用于测量黑体发出的辐射的函数,其形参为波长λ和温度T(单位:开尔文)

其中c为光速,h为普朗克常量,kb为波尔兹曼常数,k为开尔文温度

这里我们需要将波长单位从纳米转化成米。

void Blackbody(const Float *lambda, int n, Float T, Float *Le) {
    if (T <= 0) {
        for (int i = 0; i < n; ++i) Le[i] = 0.f;
        return;
    }
    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));
        CHECK(!std::isnan(Le[i]));
    }
}

斯蒂芬-玻尔兹曼定律给出了黑体发射体在点p处的发射辐射度。

其中σ是斯蒂芬玻尔兹曼常数,5.67032 × 10^−8 Wm^−2 K^−4 。请注意,所有频率上的总发射以t^4的速度快速增长。因此,如何将黑体发射器的温度加倍,那么总能量就会增加16倍。

标准光源

另一种有用的灯光发射分布类型是一个
由CIE(国际照明委员会)制定的“标准光源”标准。

灯光类接口

类名为Light,位于core/light.h与core/light.cpp中。

所有的灯光类型都有4个公共参数:

  1. flags:基本光源类型标记。例如,
    灯光是否用delta分布来描述。(这类灯的例子包括点光源,它从一个点发出照明,以及方向灯,所有的光都来自相同的方向。)蒙特卡罗算法对来自光源的光照进行采样,需要知道哪些光线是由delta分布描述的。
  2. transformation:控制坐标与旋转用的
  3. MediumInterface:介质接口。
  4. nSamples:采样数

LightFlags

类型flags枚举与对应方法

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;
}

Sample_Li

Light类必须实现一个关键方法Sample_Li()。调用者通过一个Interaction对象提供在场景中与时间关联的世界空间的参考点以及此时灯光往该位置发射的辐射度。(在假设中间没有阻挡物的情况下)

virtual Spectrum Sample_Li(const Interaction &ref, const Point2f &u,Vector3f *wi, Float *pdf, VisibilityTester *vis) const = 0;

Light类同时还负责初始化的入射方向光源ωi与VisibilityTester对象初始化。VisibilityTester保存着必须通过追踪得到的Shadow Ray的信息,以验证光线和参考点之间没有遮挡对象。

对于某些类型的灯光,光线可以从许多方向到达参考点。不像点光源那样只有一个方向。对于这些类型的光源,就需要使用Sample_Li()方法对光源表面进行采样。之后再用蒙特卡洛方法求积分来得到对应位置的分布。

所有类型的Light类都必须能够返回总发射功率。这个变量对光线传输算法很有用,因为光线传输算法想要尽可能得将计算资源用在对场景贡献最大的灯光上。

virtual Spectrum Power() const = 0;

Preprocess

Light类在被渲染前会调用接口Preprocess()。它将场景作为参数,使得光源在开始渲染前就能确定场景特征。

可见性测试

VisibilityTester是一个封闭的对象,它封装了少量的数据和一些尚未完成的计算。它允许光在参考点和光源相互可见的情况下下返回一个辐射值。

积分器可以在追踪阴影射线之前判断来自入射方向的照明是否相关。例如,在非半透明表面背面入射的光对于来自另一侧的反射没有任何贡献。

VisibilityTesters由两个Interaction对象来创建。为用于光线追踪的阴影射线的两个端点。因为一个Interaction对象已经被使用,因此不需要特殊情况来计算表面参考点与参与介质中的参考点之间的可见性。

Light提供了两种方法来确定这两个点之间的可见性。一种是Unoccluded():它跟踪两者之间的阴影射线并返回布尔值。
因为它只返回一个布尔值,Unoccluded()也会忽略光线通过的任何散射介质对其所携带的亮度的影响。

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

因为它只返回一个布尔值,Unoccluded()也会忽略光线通过的任何散射介质对其所携带的亮度的影响。积分器需要考虑到这种情况,所以他们使用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;
}

如果在射线段上发现一个交点,且入射面是不透明的,则射线被阻挡,透射率为零。

点光源

点光源是各向同性的,各个方向发射出的能量都是一样的。类名为PointLight,位于lights/point.h与lights/point.cpp中。

在构造函数中,PointLight存储了世界空间与本地空间相互转换的矩阵,以及光源的强度,单位是功率/立面角。又因为是各项同性光源,所以点光源的光源强度是个常数。因为点光源表示只从一个位置发出光的奇点,所以light::flags字段被初始化为LightFlags::DeltaPosition。

严格地来说,用亮度单位来描述点光源是不正确,辐射强度才是更为合适的描述点发射源的单位。所以在这里,我们将使用Sample_Li这个通用接口来计算到达某点的辐射照度,用辐射强度除以距离的平方获得。

Spectrum PointLight::Sample_Li(const Interaction &ref, const Point2f &u,Vector3f *wi, Float *pdf,VisibilityTester *vis) const {
    ProfilePhase _(Prof::LightSample);
    *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;
}

聚光灯

类名为SpotLight,位于lights/spot.h与lights/spot.cpp中。聚光灯是从点光源衍生过来的一种光源类型,它在所在的位置发出一个方向锥状的光。其构造函数接受锥体角宽度与衰减开始距离两个值,之后分别计算并储存两者的余弦值。两个变量的定义具体详见图12.8。

SpotLight::Sample_Li()几乎与PointLight::Sample_Li()相同。不同之处在于SpotLight调用了Falloff()方法,该方法计算聚光灯锥的光分布。

为了计算点p接受到的聚光灯辐射强度,第一步就是计算从聚光灯原点到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);
}

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

贴图透射灯

类名为ProjectionLight,定义在lights/projection.cpp与lights/projection.h。它将一个贴图映射并投影到场景中。ProjectionLight类使用投影转换,根据构造函数的fov参数,将场景中的点投影到光线的投影平面上。

然而,拥有精确表现的投影函数,就好比使用具有MIPMap的图片一样,能够使用蒙特卡罗技术进行采样对精确表现投影分布非常有用。

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));
    hither = 1e-3f;
    yon = 1e30f;
    lightProjection = Perspective(fov, hither, yon);

    Transform screenToLight = Inverse(lightProjection);
    Point3f pCorner(screenBounds.pMax.x, screenBounds.pMax.y, 0);
    Vector3f wCorner = Normalize(Vector3f(screenToLight(pCorner)));
    cosTotalWidth = wCorner.z;
}

与Spotlight类似,ProjectionLight::Sample_Li()调用了Projection(),用以计算对应方向的投影结果。因为投影变换具有将投影中心后面的点投影到它前面的性质。所以去掉z值为负的点是很重要的。如果不进行这种检查,就不可能知道投影点原来是在灯后面(因为没有被照亮)还是在灯前面。

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

Spectrum ProjectionLight::Projection(const Vector3f &w) const {
    Vector3f wl = WorldToLight(w);
    // Discard directions behind projection light
    if (wl.z < hither) 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);
}

这种灯的功率与聚光灯近似。

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

方向光

类名为DistantLight,定义于lights/distant.cpp与lights/distant.cpp中。注意,DistantLight构造函数不接受mediluminterface参数;对于遥远的光来说,唯一合理的介质是真空。如果它本身在介质中,那么它所有的辐射都会被介质吸收,因为它是无限远的。

一些距离光的方法需要知道场景的边界。因为灯光是在场景几何体之前创建的,所以当DistantLight构造函数运行时,这些边界是不可用的。因此,DistantLight实现了可选的Preprocess()方法来获取界限。(该方法在场景构造函数的末尾调用。)

DistantLight::Sample_Li()的实现都较为简单,唯一有趣的是:VisibilityTester的初始化:阴影光线的第二个点设置在沿着远光源入射方向距离为场景边界球半径的两倍的位置,这样就保证了第二个点在场景边界之外。

Spectrum DistantLight::Sample_Le(const Point2f &u1, const Point2f &u2,Float time, Ray *ray, Normal3f *nLight,Float *pdfPos, Float *pdfDir) const {
    ProfilePhase _(Prof::LightSample);
    // Choose point on disk oriented toward infinite light direction
    Vector3f v1, v2;
    CoordinateSystem(wLight, &v1, &v2);
    Point2f cd = ConcentricSampleDisk(u1);
    Point3f pDisk = worldCenter + worldRadius * (cd.x * v1 + cd.y * v2);

    // Set ray origin and direction for infinite light ray
    *ray = Ray(pDisk + worldRadius * wLight, -wLight, Infinity, time);
    *nLight = (Normal3f)ray->d;
    *pdfPos = 1 / (Pi * worldRadius * worldRadius);
    *pdfDir = 1;
    return L;
}

方向光与别的类型灯光不同,它的发射功率与场景空间范围有关。事实上,它与接收光线的场景面积成正比。为了理解为什么会这样,考虑一个面积为A的圆盘,被辐射亮度为L的方向光以圆盘法线方向照亮。到达圆盘的总功率为,与接收面的面积成正比。

但是为了计算表面积而去计算对光源可见物体总的表面积是不切实际的,所以我们将用一个圆盘来进行近似计算。

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

面光源

面光源是由一个或多个形状的光源组成的,这些形状从其表面发射光辐射,在表面的每个点上有一定的方向分布的辐射。一般来说,计算与面积光有关的辐射量需要计算光表面的积分,而这些积分通常不能以闭合形式计算。类名为AreaLight,定义于core/light.h与core/light.cpp中。

AreaLight添加了新的方法L(),实现了对给定与表面相交的位置,估算在给定方向上光源发射辐射量L。为了方便起见,在SurfaceInteraction类中有Le(),它可以轻松计算射线相交的表面点的辐射亮度。

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

DiffuseAreaLight类实现了一个基础面光源,具有均匀的空间和方向辐射分布。它发射辐射由形状决定,默认情况下,只有法线正方向的那个面发光,当然可以设置Shape::reverseOrientation为true,通过反转法线的方式来切换发光方向。定于与light/diffuse.h与light/diffuse.cpp中。

因为这中面光源只从形状表面的一侧发出光,所以它的L()方法只是确保出射的方向与法线位于同一个半球。

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

DiffuseAreaLight::Sample_Li()方法不像之前描述的光源那样简单了。具体地说,在场景中的每个点,区域光的辐射可以从多个方向入射,而不像其他光那样只从一个方向入射。这就需要使用到蒙特罗卡方法了,在之后的章节会解释。因为表面发出的辐射是均匀的,所以辐射功率可以这么计算:

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

无穷大的面光源(环境球)

无穷大的面光源是另一种有用的光源。可以把这种光源当成一个向各个方向投射光线的巨大球体(它包围了整个场景)。它的主要用途场景照明。因为是无穷大的,所以其介质指针为空。类名为:InfiniteAreaLight,定义于lights/infinite.h与lights/infinite.cpp中。

InfiniteAreaLight类具有一个变换矩阵成员变量,用于定位图像映射。先使用球坐标方程讲球体映射到(θ,φ)方向,之后再转换到(u,v)坐标。

因为InfiniteAreaLight的光线会向各个方向射出,所以它必须使用蒙特卡洛方法进行采样。

与定向光一样,InfiniteAreaLight的总功率与场景的表面积有关。这里依然使用近似计算。

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

pbrt为这个类实现了Le()接口,为这个场景提供环境光照,而其他光源中在这个接口中返回0。