3.11 交互(Interactions

本章中的最后两个抽象,SurfaceInteractionMediumInteraction,分别表示在表面上和参与介质中的点的局部信息。例如,第 6 章中的光线-形状相交例程用一个 SurfaceInteraction 返回有关相交点的局部微分几何的信息。随后,第 10 章中的纹理代码使用 SurfaceInteraction 中的值来计算材料属性。与之密切相关的 MediumInteraction 类被用来表示光与参与介质(如烟雾或云)相互作用的点。这些类的所有实现都在文件 interaction.hinteraction.cpp 中。

SurfaceInteractionMediumInteraction 都继承自一个通用的 Interaction 类,该类提供了通用的成员变量和方法,这使得系统中对于表面和介质交互之间差异不重要的部分可以纯粹通过 Interaction 来实现。

/** Interaction 定义 */
class Interaction {
  public:
    /** Interaction 公有方法 */
    Interaction() = default;
    Interaction(Point3fi pi, Normal3f n, Point2f uv, Vector3f wo, Float time)
        : pi(pi), n(n), uv(uv), wo(Normalize(wo)), time(time) {}
    Point3f p() const { return Point3f(pi); }
    bool IsSurfaceInteraction() const { return n != Normal3f(0, 0, 0); }
    bool IsMediumInteraction() const { return !IsSurfaceInteraction(); }
    const SurfaceInteraction &AsSurface() const {
        CHECK(IsSurfaceInteraction());
        return (const SurfaceInteraction &)*this;
    }
    SurfaceInteraction &AsSurface() {
        CHECK(IsSurfaceInteraction());
        return (SurfaceInteraction &)*this;
    }
    // used by medium ctor
    PBRT_CPU_GPU
    Interaction(Point3f p, Vector3f wo, Float time, Medium medium)
        : pi(p), time(time), wo(wo), medium(medium) {}
    PBRT_CPU_GPU
    Interaction(Point3f p, Normal3f n, Float time, Medium medium)
        : pi(p), n(n), time(time), medium(medium) {}
    PBRT_CPU_GPU
    Interaction(Point3f p, Point2f uv)
        : pi(p), uv(uv) {}
    PBRT_CPU_GPU
    Interaction(const Point3fi &pi, Normal3f n, Float time = 0,
                Point2f uv = {})
        : pi(pi), n(n), uv(uv), time(time) {}
    PBRT_CPU_GPU
    Interaction(const Point3fi &pi, Normal3f n, Point2f uv)
        : pi(pi), n(n), uv(uv) {}
    PBRT_CPU_GPU
    Interaction(Point3f p, Float time, Medium medium)
        : pi(p), time(time), medium(medium) {}
    PBRT_CPU_GPU
    Interaction(Point3f p, const MediumInterface *mediumInterface)
        : pi(p), mediumInterface(mediumInterface) {}
    PBRT_CPU_GPU
    Interaction(Point3f p, Float time, const MediumInterface *mediumInterface)
        : pi(p), time(time), mediumInterface(mediumInterface) {}
    PBRT_CPU_GPU
    const MediumInteraction &AsMedium() const {
        CHECK(IsMediumInteraction());
        return (const MediumInteraction &)*this;
    }
    PBRT_CPU_GPU
    MediumInteraction &AsMedium() {
        CHECK(IsMediumInteraction());
        return (MediumInteraction &)*this;
    }
    
    std::string ToString() const;
    Point3f OffsetRayOrigin(Vector3f w) const {
        return pbrt::OffsetRayOrigin(pi, n, w);
    }
    Point3f OffsetRayOrigin(Point3f pt) const {
        return OffsetRayOrigin(pt - p());
    }
    RayDifferential SpawnRay(Vector3f d) const {
        return RayDifferential(OffsetRayOrigin(d), d, time, GetMedium(d));
    }
    Ray SpawnRayTo(Point3f p2) const {
        Ray r = pbrt::SpawnRayTo(pi, n, time, p2);
        r.medium = GetMedium(r.d);
        return r;
    }
    PBRT_CPU_GPU
    Ray SpawnRayTo(const Interaction &it) const {
        Ray r = pbrt::SpawnRayTo(pi, n, time, it.pi, it.n);
        r.medium = GetMedium(r.d);
        return r;
    }
    Medium GetMedium(Vector3f w) const {
        if (mediumInterface)
            return Dot(w, n) > 0 ? mediumInterface->outside :
                                    mediumInterface->inside;
        return medium;
    }
    Medium GetMedium() const {
        return mediumInterface ? mediumInterface->inside : medium;
    }
    
    /** Interaction 公有成员 */
    Point3fi pi;
    Float time = 0;
    Vector3f wo;
    Normal3f n;
    Point2f uv;
    const MediumInterface *mediumInterface = nullptr;
    Medium medium = nullptr;
    
};

有多种 Interaction 构造函数可用;根据所构建的交互类型以及相关的信息类型,接受相应的参数集。下面这个其中最通用的一种。

/** Interaction 公有方法 */
Interaction(Point3fi pi, Normal3f n, Point2f uv, Vector3f wo, Float time)
    : pi(pi), n(n), uv(uv), wo(Normalize(wo)), time(time) {}

所有交互都有一个与它们相关的点 \( \text{p} \)。这个点使用 Point3fi 类进行存储,该类使用 Interval 来表示每个坐标值。存储一小段浮点值而不是单个 Float 使得能够表示交点的数值误差的边界,这种情况发生在通过光线交点计算得出点 \( \text{p} \) 时。该信息将有助于避免光线离开表面时出现不正确的自交,如第 6.8.6 节将讨论的。

/** Interaction 公有成员 */
Point3fi pi;

Interaction 提供了一种便捷的方法,返回一个常规的 Point3f ,用于系统中不需要考虑任何误差的交互点(例如,纹理评估例程)。

/** Interaction 公有方法 */
Point3f p() const { return Point3f(pi); }

所有交互也都有一个与它们相关的时间。除了其他用途外,这个值对于设置离开交互的生成光线(a spawned ray leaving the interaction)的时间是必要的。

/** Interaction 公有成员 */
Float time = 0;

对于沿光线的交互(无论是来自光线与形状的交点还是光线穿过参与介质),负光线方向存储在成员变量 wo 中,对应于 \( \omega_\text{o} \) ,这是我们在计算光照时用于表示出射方向的符号。对于其他类型的交互点,其中出射方向的概念不适用的(例如,通过随机采样形状表面上的点求出的点),wo 的值为 \( (0,0,0) \)。

/** Interaction 公有成员 */
Vector3f wo;

对于表面上的交互,n 存储了该点的表面法线,而 uv 存储了其 \( (u,v) \) 参数坐标。可以合理地问,为什么这些值存储在基类 Interaction 中而不是在 SurfaceInteraction 中?原因是系统中有一些部分 通常 不关心表面和介质交互之间的区别——例如,一些在要照亮给定点的光源进行采样点的例程。如果这些值可用,它们会被使用;如果设置为零,则会被忽略。通过接受这里将它们放在错误位置的小不和谐,这些方法的实现和调用它们的代码变得更加简单。

/** Interaction 公有成员 */
Normal3f n;
Point2f uv;

可以检查指向 Interaction 的指针或引用是否属于两个子类之一。非零的表面法线被用作表面的区分符。

/** Interaction 公有方法 */
bool IsSurfaceInteraction() const { return n != Normal3f(0, 0, 0); }
bool IsMediumInteraction() const { return !IsSurfaceInteraction(); }

还提供了将类型转换为子类类型的方法。这是进行运行时检查的好地方,以确保请求的转换是有效的。此方法的 non-const 变体和对应的 AsMedium() 方法都与之类似,未包含在文本中。

/** Interaction 公有方法 */
const SurfaceInteraction &AsSurface() const {
    CHECK(IsSurfaceInteraction());
    return (const SurfaceInteraction &)*this;
}

交互也可以表示两种使用第 11.4 节中定义的 MediumInterface 类实例的参与介质之间的接口,或在其点上使用 Medium 的散射介质的属性。在这里,Interaction 的抽象也会泄漏(abstraction leaks):表面可以表示介质之间的接口,而在介质内部的某一点上,虽然没有接口,但存在当前介质。这两个值都存储在 Interaction 中,出于与 nuv 相同的便利原因。

/** Interaction 公有成员 */
const MediumInterface *mediumInterface = nullptr;
Medium medium = nullptr;

3.11.1 表面交互(Surface Interaction)

如前所述,表面上某一点的几何结构(通常是通过将光线与表面相交计算出的位置)由 SurfaceInteraction 表示。拥有这种抽象使得系统的大部分能够处理表面上的点,而无需考虑点所处的特定几何形状。

/** SurfaceInteraction 定义 */
class SurfaceInteraction : public Interaction {
  public:
    /** SurfaceInteraction 公有方法 */
    SurfaceInteraction() = default;
    SurfaceInteraction(Point3fi pi, Point2f uv, Vector3f wo, Vector3f dpdu,
            Vector3f dpdv, Normal3f dndu, Normal3f dndv, Float time,
            bool flipNormal)
        : Interaction(pi, Normal3f(Normalize(Cross(dpdu, dpdv))), uv, wo, time),
            dpdu(dpdu), dpdv(dpdv), dndu(dndu), dndv(dndv) {
        <<Initialize shading geometry from true geometry>> 
        <<Adjust normal based on orientation and handedness>> 
    }
    SurfaceInteraction(Point3fi pi, Point2f uv, Vector3f wo,
            Vector3f dpdu, Vector3f dpdv, Normal3f dndu,
            Normal3f dndv, Float time, bool flipNormal,
            int faceIndex)
        : SurfaceInteraction(pi, uv, wo, dpdu, dpdv, dndu, dndv, time, flipNormal) {
            this->faceIndex = faceIndex;
    }
    void SetShadingGeometry(Normal3f ns, Vector3f dpdus, Vector3f dpdvs,
            Normal3f dndus, Normal3f dndvs, bool orientationIsAuthoritative) {
        <<Compute shading.n for SurfaceInteraction>> 
        <<Initialize shading partial derivative values>> 
    }
    std::string ToString() const;
    void SetIntersectionProperties(Material mtl, Light area,
            const MediumInterface *primMediumInterface, Medium rayMedium) {
        material = mtl;
        areaLight = area;
        <<Set medium properties at surface intersection>>  
    }
    PBRT_CPU_GPU
    void ComputeDifferentials(const RayDifferential &r, Camera camera,
                                int samplesPerPixel);
    PBRT_CPU_GPU
    void SkipIntersection(RayDifferential *ray, Float t) const;
    using Interaction::SpawnRay;
    RayDifferential SpawnRay(const RayDifferential &rayi, const BSDF &bsdf,
                            Vector3f wi, int /*BxDFFlags*/ flags, Float eta) const;
    BSDF GetBSDF(const RayDifferential &ray,
                SampledWavelengths &lambda, Camera camera,
                ScratchBuffer &scratchBuffer, Sampler sampler);
    BSSRDF GetBSSRDF(const RayDifferential &ray,
                SampledWavelengths &lambda, Camera camera,
                ScratchBuffer &scratchBuffer);
    PBRT_CPU_GPU
    SampledSpectrum Le(Vector3f w, const SampledWavelengths &lambda) const;

    /** SurfaceInteraction 公有成员 */
    Vector3f dpdu, dpdv;
    Normal3f dndu, dndv;
    struct {
        Normal3f n;
        Vector3f dpdu, dpdv;
        Normal3f dndu, dndv;
    } shading;
    int faceIndex = 0;
    Material material;
    Light areaLight;
    Vector3f dpdx, dpdy;
    Float dudx = 0, dvdx = 0, dudy = 0, dvdy = 0;

};

除了来自 Interaction 基类的表面参数设定中的点 \( \text{p} \)、表面法线 \( \mathbf{n} \) 和 \( (u,v) \) 坐标外,SurfaceInteraction 还存储了点的参数偏导数(parametric partial derivatives) \( \partial \text{p}/\partial u \) 和 \( \partial \text{p}/\partial v \),以及表面法线的偏导数 \( \partial\mathbf{n}/\partial u \) 和 \( \partial\mathbf{n}/\partial v \)。有关这些值的示意图,请参见图 3.30

/** SurfaceInteraction 公有成员 */
Vector3f dpdu, dpdv;
Normal3f dndu, dndv;

图 3.30:点 \( \text{p} \) 周围的局部微分几何(Local Differential Geometry)。 表面的参数偏导数 \( \partial \text{p}/\partial u \) 和 \( \partial \text{p}/\partial v \) 位于切平面内,但不一定正交。表面法线 \( \mathbf{n} \) 由 \( \partial \text{p}/\partial u \) 和 \( \partial \text{p}/\partial v \) 的叉积给出。向量 \( \partial\mathbf{n}/\partial u \) 和 \( \partial\mathbf{n}/\partial v \) 记录了当我们沿表面移动 \( u \) 和 \( v \) 时表面法线的微分变化。

这种表示隐含地假设形状具有参数描述(parametric description)——即对于某些范围的 \( (u,v) \) 值,表面上的点由某个函数 \( f \) 给出,使得 \( \text{p}=f(u,v) \) 。尽管并非所有形状都符合这一点,但 pbrt 支持的所有形状至少具有一个局部参数描述,因此我们将坚持使用参数表示,因为这一假设在其他地方也是有帮助的(例如,在第 10 章中用于纹理的抗锯齿处理)。

SurfaceInteraction 的构造函数接受参数来设置所有这些值。它通过偏导数的叉积计算法线。

/** SurfaceInteraction 公有方法 */
SurfaceInteraction(Point3fi pi, Point2f uv, Vector3f wo, Vector3f dpdu,
        Vector3f dpdv, Normal3f dndu, Normal3f dndv, Float time,
        bool flipNormal)
    : Interaction(pi, Normal3f(Normalize(Cross(dpdu, dpdv))), uv, wo, time),
      dpdu(dpdu), dpdv(dpdv), dndu(dndu), dndv(dndv) {
    /** 从真实几何初始化着色几何(Initialize shading geometry from true geometry) */
    /** 根据方向和手性调整法线(Adjust normal based on orientation and handedness) */
}

SurfaceInteraction 存储了一个表面法线的第二个实例以及各种偏导数,以表示这些量的可能受到扰动的值——这些值可以通过凹凸映射(bump mapping)或与网格的每个顶点法线插值生成。系统的某些部分使用这种着色几何(shading geometry),而其他部分则需要处理原始量。

/** SurfaceInteraction 公共成员 */
struct {
    Normal3f n;
    Vector3f dpdu, dpdv;
    Normal3f dndu, dndv;
} shading;

着色几何的值在构造函数中初始化,以匹配原始表面几何。如果存在着色几何,通常在 SurfaceInteraction 的构造函数运行后的一段时间内不会计算。稍后将定义的 SetShadingGeometry() 方法更新着色几何。

/** 从真实几何初始化着色几何 */
shading.n = n;
shading.dpdu = dpdu;
shading.dpdv = dpdv;
shading.dndu = dndu;
shading.dndv = dndv;

表面法线对 pbrt 具有特殊意义,它假设对于封闭形状(closed shapes),法线的方向指向形状的外部。对于用作面光源(area light source)的几何体,光默认仅从法线指向的一侧发出;另一侧为黑色。由于法线具有这种特殊意义,pbrt 提供了一种机制,允许用户反转法线的方向,使其指向相反的方向。在 pbrt 输入文件中的 ReverseOrientation 指令(directive)会将法线翻转,使其指向相反、非默认的方向。因此,有必要检查给定的 Shape 是否设置了相应的标志,如果是,则在此处切换法线的方向。

然而,还有一个因素影响法线的方向,也必须在这里考虑。如果一个形状的变换矩阵将对象坐标系的手性从 pbrt 的默认左手坐标系切换到右手坐标系,我们也需要切换法线的方向。要理解为什么会这样,考虑一个缩放矩阵 \( \mathbf{S}(1,1,-1) \) 。我们自然会期望这个缩放会切换法线的方向,尽管因为我们是通过 \( \mathbf{n} = \partial\text{p}/\partial u \times \partial\text{p}/\partial v \) 计算法线的,

\[ \begin{align} \mathbf{S}(1,1,-1)\frac{\partial\text{p}}{\partial u} \times \mathbf{S}(1,1,-1)\frac{\partial\text{p}}{\partial v} &= \mathbf{S}(-1,-1,1)\left(\frac{\partial\text{p}}{\partial u} \times \frac{\partial\text{p}}{\partial v} \right) \\ &= \mathbf{S}(-1,-1,1)\mathbf{n} \\ &\neq \mathbf{S}(1,1,-1)\mathbf{n} \end{align} \]

因此,如果变换改变了坐标系统的手性,则有必要翻转法线的方向,因为使用叉积计算法线方向时不会考虑到这一翻转。调用者传递的标志指示是否需要进行此翻转。

/** 根据方向和手性调整法线 */
if (flipNormal) {
    n *= -1;
    shading.n *= -1;
}

pbrt 还提供了将整数索引与多边形网格的每个面关联的能力。这些信息用于某些纹理映射操作。一个单独的 SurfaceInteraction 构造函数允许指定它。

/** SurfaceInteraction 公有成员 */
int faceIndex = 0;

当计算着色坐标框架(shading coordinate frame)时,SurfaceInteraction 通过其 SetShadingGeometry() 方法进行更新。

/** SurfaceInteraction 公有方法 */
void SetShadingGeometry(Normal3f ns, Vector3f dpdus, Vector3f dpdvs,
        Normal3f dndus, Normal3f dndvs, bool orientationIsAuthoritative) {
    /** 计算 SurfaceInteraction 的 shading.n */
    shading.n = ns;
    if (orientationIsAuthoritative)
        n = FaceForward(n, shading.n);
    else
        shading.n = FaceForward(shading.n, n);

    /** 初始化着色偏导数的值 */
    shading.dpdu = dpdus;
    shading.dpdv = dpdvs;
    shading.dndu = dndus;
    shading.dndv = dndvs;
    
}

在执行与之前相同的叉乘(并可能翻转法线的朝向)以计算初始着色法线后,实现接着会根据需要翻转着色法线或真实几何法线,以确保这两个法线位于同一半球内。由于着色法线通常表示几何法线的相对较小的扰动(perturbation),因此它们应该始终位于同一半球内。根据上下文,几何法线或着色法线中的一个可能更权威地指向表面的正确“外侧”,因此调用者传递一个布尔值,以确定在需要时应翻转哪一个。

/** 计算 SurfaceInteraction 的 shading.n */
shading.n = ns;
if (orientationIsAuthoritative)
    n = FaceForward(n, shading.n);
else
    shading.n = FaceForward(shading.n, n);

在设置法线后,各种偏导数可以被复制。

/** 初始化着色偏导数的值 */
shading.dpdu = dpdus;
shading.dpdv = dpdvs;
shading.dndu = dndus;
shading.dndv = dndvs;

3.11.2 介质互动(Medium Interaction)

如前所述,MediumInteraction 类用于表示在散射介质(如烟雾或云)中的某一点的交互。

/** MediumInteraction 定义 */
class MediumInteraction : public Interaction {
  public:
    /** MediumInteraction 公有方法 */
    MediumInteraction(Point3f p, Vector3f wo, Float time, Medium medium,
                    PhaseFunction phase)
    : Interaction(p, wo, time, medium), phase(phase) {}
    std::string ToString() const;

    /** MediumInteraction 公有成员 */
    PhaseFunction phase;

};

SurfaceInteraction 相比,它对基类 Interaction 添加的内容很少。唯一的补充是一个 PhaseFunction ,它描述了介质中的粒子如何散射光。相位函数和 [PhaseFunction] 类在第 11.3 节中介绍。

/** MediumInteraction 公有方法 */
MediumInteraction(Point3f p, Vector3f wo, Float time, Medium medium,
                  PhaseFunction phase)
    : Interaction(p, wo, time, medium), phase(phase) {}
/** MediumInteraction 公有成员 */
PhaseFunction phase;