1.3 System Overview系统概述 - Physically Based Rendering From Theory To Implementation(PBRT)

Physically Based Rendering From Theory To Implementation

翻译:https://pbr-book.org/4ed/Introduction/pbrt_System_Overview#fragment-Convertcommand-lineargumentstovectorofstrings-0

1.3 System Overview系统概述

1.3.1 Phases of Execution 执行阶段

PBRT可以在概念上分为三个执行阶段。首先,解析用户提供的场景描述文件。场景描述是一个文本文件,它指定了构成场景的几何形状、它们的材料属性、照亮它们的灯光、虚拟摄像机在场景中的位置,以及整个系统中使用的所有单个算法的参数。场景文件格式在pbrt网站pbrt.org上有文档。

解析阶段的结果是BasicScene类的一个实例,它存储场景规范,但不是适合渲染的形式。在执行的第二阶段,pbrt创建与场景相对应的特定对象;例如,如果指定了透视投影,则在此阶段将创建与指定的观看参数相对应的PerspectiveCamera对象。以前版本的pbrt混合了前两个阶段,但对于这个版本,我们将它们分开,因为CPU和GPU渲染路径在内存中表示场景的一些方式不同。

在第三阶段,执行主呈现循环。这个阶段是pbrt通常花费大部分运行时间的地方,本书的大部分内容都致力于在这个阶段执行的代码。为了编排呈现,pbrt实现了一个积分器,之所以这样命名是因为它的主要任务是计算公式(1.1)中的积分。

1.3.2 pbrt’s main() Function

pbrt可执行文件的main()函数在保存pbrt源代码的目录下的cmd/pbrt.cpp文件中定义,Src /pbrt在pbrt中分布。它只有150行左右的代码,其中大部分用于处理命令行参数和相关的记录(bookkeeping)。
在这里插入图片描述
pbrt不是直接对提供给main()函数的argv值进行操作,而是将提供的参数转换为std::string的向量。它这样做不仅是为了字符串类的更大便利,而且也是为了支持非ascii字符集。第B.3.2节提供了关于字符编码以及在pbrt中如何处理字符编码的更多信息。

//<<Convert command-line arguments to vector of strings>>= 
std::vector<std::string> args = GetCommandLineArguments(argv);

我们将只在本书文本中包括一些主要函数片段的定义。有些代码,比如处理用户提供的命令行参数解析的代码,既简单又长,不值得花那么多页来增加本书的篇幅。但是,我们将包括声明存储选项值的变量的片段。

//<<Declare variables for parsed command line>>= 
PBRTOptions options;
std::vector<std::string> filenames;

PBRTOptions 类存储了各种渲染选项,这些选项通常更适合在命令行中指定,而不是在场景描述文件中指定—例如,pbrt在渲染过程中应该如何描述其进度。它被传递给InitPBRT()函数,该函数聚合在任何其他工作完成之前必须执行的各种系统范围的初始化任务。例如,它初始化日志系统并启动一组用于并行化pbrt的线程。

在解析和验证了参数之后,ParseFiles()函数接管处理前面描述的三个执行阶段中的第一个阶段。在两个类的帮助下,BasicSceneBuilder和BasicScene(分别在第C.2节和C.3节中描述),它循环遍历提供的文件名,依次解析每个文件。如果pbrt在没有提供文件名的情况下运行,它将从标准输入中查找场景描述。标记和解析场景描述文件的机制将不会在本书中描述,但是解析器的实现可以在src/pbrt目录下的parser.h和parser.cpp文件中找到。

//<<Parse provided scene description files>>= 
BasicScene scene;
BasicSceneBuilder builder(&scene);
ParseFiles(&builder, filenames);

在场景描述被解析后,两个函数中的一个被调用来渲染场景。RenderWavefront()同时支持CPU和GPU渲染路径,并行处理一百万左右的图像样本。这是第十五章的主题。RenderCPU()使用Integrator实现渲染场景,并且仅在CPU上运行时可用。它使用的并行性比RenderWavefront()少得多,只渲染与CPU线程并行的图像样本一样多。

这两个函数都是从将BasicScene转换为适合于高效渲染的形式开始的,然后将控制传递给特定于处理器的集成器。(关于这个过程的更多信息见C.3节。)现在我们将忽略这个转换的细节,以便专注于RenderCPU()中的主渲染循环,这要有趣得多。为此,我们将把高效的场景表示作为给定的。

	// Render the scene
    if (Options->useGPU || Options->wavefront)
         RenderWavefront(scene);
    else
         RenderCPU(scene);

在渲染图像之后,CleanupPBRT()负责优雅地关闭系统,包括,例如,终止由InitPBRT()启动的线程。

        // Clean up after rendering the scene
        CleanupPBRT();

1.3.3 积分器接口

在RenderCPU()渲染路径中,一个实现了Integrator接口的类的实例负责渲染。因为Integrator的实现只能在CPU上运行,所以我们将Integrator定义为一个具有纯虚方法的标准基类。Integrator及其各种实现分别定义在cpu/ Integrator .h和cpu/ Integrator .cpp文件中。

<<Integrator Definition>>= 
class Integrator {
  public:
    <<Integrator Public Methods>> 
    <<Integrator Public Members>> 
  protected:
    <<Integrator Protected Methods>> 
};

基本的Integrator构造函数接受一个表示场景中所有几何对象的Primitive(基元),以及一个保存场景中所有灯光的数组。

    protected:
	<<Integrator Protected Methods>>= 
	Integrator(Primitive aggregate, std::vector<Light> lights)
	    : aggregate(aggregate), lights(lights) {
	    <<Integrator constructor implementation>> 
	}

场景中的每个几何对象都由一个基元表示,它主要负责组合指定其几何形状的形状和描述其外观的材料(例如,对象的颜色,或是否有暗或有光泽)。接着,场景中的所有几何基元被收集到单个聚合基元中,存储在Integrator::aggregate成员变量中。这个聚合是一种特殊的基本类型,它本身包含对许多其他基本类型的引用。聚合实现将所有场景的基元存储在一个加速数据结构中,以减少与远离给定光线的基元进行不必要的光线相交测试的数量。因为它实现了基本类型接口,所以与系统的其他部分相比,它看起来没有什么不同。

场景中的每个光源都由一个实现光接口的对象表示,它允许光指定其形状和它发射的能量分布。一些光源需要知道整个场景的包围盒,这在它们第一次创建时是不可用的。因此,Integrator构造函数调用它们的Preprocess()方法,提供这些包围盒。此时,任何“无限远的”光源也存储在一个单独的数组中。这种光将在12.5节中介绍,它模拟无限远的光源,例如在地球表面接收到的天空光源,这是一个合理的模型。有时候我们需要遍历这些光源,而对于具有数千个光源的场景来说,为了找到这些光源而遍历所有光源是低效的。

    // Integrator Protected Methods
    Integrator(Primitive aggregate, std::vector<Light> lights)
        : aggregate(aggregate), lights(lights) {
        // Integrator constructor implementation
        for (auto &light : lights) {
            light.Preprocess(sceneBounds);
            if (light.Type() == LightType::Infinite)
                infiniteLights.push_back(light);
        }
    }

积分器必须提供Render()方法的实现,该实现没有其他参数。场景初始化后,RenderCPU()函数会调用这个方法。积分器的任务是渲染场景指定的集合和灯光。除此之外,它是由特定的积分器来定义渲染场景的方式,使用它需要的任何其他类(例如,相机模型)。这个接口非常通用,以允许广泛的实现——例如,可以实现一个积分器,只在场景中分布的稀疏点集上测量光线,而不是生成常规的2D图像。

class Integrator {
public:
	virtual void Render() = 0;
}

Integrator类提供了两个光线-基元相交相关的方法来使用它的子类。Intersect()接收一条射线和一个最大参数距离tMax,跟踪给定的射线到场景中,并返回一个ShapeIntersection对象,对应于射线碰到的最近的基元(如果在tMax之前有一个交集)。(ShapeIntersection结构在6.1.3节中定义。)需要注意的一点是,这个方法的返回值类型是pstd::optional,而不是c++标准库中的std::optional;我们在pstd命名空间中重新实现了标准库的部分代码,原因将在1.5.5节讨论。

class Integrator {
public:
	virtual void Render() = 0;
	pstd::optional<ShapeIntersection> Intersect(const Ray &ray,Float tMax = Infinity) const;
}

// Intersect定义
pstd::optional<ShapeIntersection> Integrator::Intersect(const Ray &ray,Float tMax) const 
{
    if (aggregate)
        return aggregate.Intersect(ray, tMax);
    else
        return {};
}

还要注意Intersect()方法中的大写浮点类型Float: pbrt中的几乎所有浮点值都声明为Float。(唯一的例外是在一些特殊情况下需要32位浮点数或64位双精度数(例如,当将二进制值保存到文件中时)。)根据pbrt的编译标志,Float是Float或double的别名,尽管在实践中单精度Float几乎总是足够的。Float的定义在pbrt.h头文件中,它包含在pbrt中的所有其他源文件中。

#ifdef PBRT_FLOAT_AS_DOUBLE
    using Float = double;
#else
    using Float = float;
#endif

Integrator::IntersectP()与Intersect()方法密切相关。它检查光线是否存在交点,但只返回一个布尔值,表示是否找到了交点。(名称中的“P”表明它是一个计算谓词的函数,使用Lisp编程语言的常见命名约定。)由于IntersectP()不需要查找最近的交点,也不需要返回关于交点的额外几何信息,所以它通常比Integrator::Intersect()更高效。这个方法用于阴影射线。

bool Integrator::IntersectP(const Ray &ray, Float tMax) const {
    if (aggregate)
        return aggregate.IntersectP(ray, tMax);
    else
        return false;
}

1.3.4 ImageTileIntegrator和主渲染循环

在实现一个模拟光传输以呈现图像的基本积分器之前,我们将定义两个积分器子类,它提供了该积分器和许多集成器实现所使用的额外的共同功能。我们从ImageTileIntegrator开始,它从Integrator继承。下一节定义RayIntegrator,它从ImageTileIntegrator继承。

所有pbrt的基于cpu的积分器都使用相机模型渲染图像来定义这些Viewing参数,并且所有并行渲染都是通过将图像分割成块(tiles)来实现的,并让不同的处理器处理不同的块(tiles)。因此,pbrt包含为这些任务(tasks)提供公共功能的ImageTileIntegrator。

class ImageTileIntegrator : public Integrator {
  public:
    <<ImageTileIntegrator Public Methods>> 
  protected:
    <<ImageTileIntegrator Protected Members>> 
};

除了aggregate和lights, ImageTileIntegrator构造函数还需要一个相机,用于指定视角和镜头参数,如位置、方向、焦点和可视范围。 Film 类存储相机处理的图像数据。Camera类是第5章大部分内容的主题,Film在第5.4节中描述。Film负责将最终图像写入文件。

构造函数还接受一个采样器参数Sampler; 它的作用比较微妙,但是它的实现可以从根本上影响系统生成的图像的质量。首先,采样器负责选择图像平面上的点,这些点决定了哪些光线最初被发射到场景中。其次,它负责提供随机样本值,积分器使用这些值来估计光传输积分的值,公式(1.1)。例如,一些积分器需要在光源上随机选择点来从面灯源计算照明。生成这些样本的良好分布是渲染过程的一个重要部分,可以从根本上影响整体效率;这是第8章的重点。

class ImageTileIntegrator : public Integrator {
  public:
    // ImageTileIntegrator Public Methods
    ImageTileIntegrator(
		    		Camera camera, 
		    		Sampler sampler, 
		    		Primitive aggregate,
		            std::vector<Light> lights)
        : 	Integrator(aggregate, lights), 
        	camera(camera), 
        	samplerPrototype(sampler) 
     {}
  protected:
    // ImageTileIntegrator Protected Members
    Camera camera;
    Sampler samplerPrototype;
}

对于所有pbrt的积分器,每个像素的最终颜色计算基于随机采样算法。如果每个像素的最终值被计算为多个样本的平均值,那么图像的质量就会提高。在低样本数量下,采样误差表现为图像中的粒状高频噪声,但随着样本数量的增加,误差以可预测的速度下降。(该主题将在2.1.4节更深入地讨论。)因此,ImageTileIntegrator::Render()以每个像素几个样本的循环(waves 原文中使用wave波,但这里认为翻译成循环更容易理解)来渲染图像。对于前两个循环,每个像素只采集一个样本。在下一循环中,取两个样本,当每一次循环结束后,下一次循环样本数量增加一倍。虽然像素循环渲染和逐像素渲染对最终图像没有区别,但是,这种计算方式意味着在渲染期间可以看到最终图像的预览,所有像素都有少量样本,而不是少数像素有所有样本,其余像素没有。

因为pbrt可以使用多个线程并行运行,所以使用这种方法需要达到一个平衡。对于线程来说,获取新图像块的工作是有代价的,一些线程在每一循环结束时,一旦没有更多的工作要做,它们就会空闲,但其他线程仍在处理分配给它们的块。这些事项导致了单次循环时间(approach)上限翻倍。

<<ImageTileIntegrator Method Definitions>>= 
void ImageTileIntegrator::Render() {
    <<Declare common variables for rendering image in tiles>> 
    <<Render image in waves>> 
}

在渲染开始之前,还需要一些额外的变量。首先,积分器实现在计算每条射线的贡献时,需要分配少量的临时内存来存储表面散射特性。大量的内存分配很容易使系统常规的内存分配例程(例如new)不堪重负,后者必须协调多线程对复杂数据结构的维护,以跟踪空闲内存。一个简单的实现可能会在内存分配器上花费相当大一部分的计算时间。

为了解决这个问题,pbrt提供了一个ScratchBuffer类来管理一个小的预分配内存缓冲区。ScratchBuffer的分配非常高效,只需要增加一个偏移量。ScratchBuffer不允许独立地释放内存分配。相反,所有内存都必须立即释放,但这样做只需要重置该偏移量。

因为多个线程同时使用scratchbuffer是不安全的,所以使用ThreadLocal模板类为每个线程创建一个单独的scratchbuffer。它的构造函数接受一个lambda函数,该函数返回它管理的类型对象的一个新实例;在这里,调用默认的ScratchBuffer构造函数就足够了。然后ThreadLocal负责为每个线程维护对象的不同副本,并按需分配它们。

void ImageTileIntegrator::Render() {
	// Declare common variables for rendering image in tiles
    ThreadLocal<ScratchBuffer> scratchBuffers([]() { return ScratchBuffer(); });
}

大多数采样器的实现都发现维护某些状态很有用,例如当前像素的坐标。这意味着多个线程不能同时使用一个采样器,ThreadLocal也用于采样器管理。Sampler提供了Clone()方法来创建其Sampler类型的新实例。Sampler首先提供ImageTileIntegrator构造函数,samplerPrototype在这里提供了这些副本。

void ImageTileIntegrator::Render() {
	// Declare common variables for rendering image in tiles
    ThreadLocal<ScratchBuffer> scratchBuffers([]() { return ScratchBuffer(); });
    ThreadLocal<Sampler> samplers([this]() { return samplerPrototype.Clone(); });
}

它有助于向用户显示已经完成了多少渲染工作,以及估计需要多长时间。这个任务由ProgressReporter类处理,它的第一个参数是工作项的总数。这里,总工作量是每个像素的采样数量乘以总像素数量。使用64位精度来计算这个值很重要,因为32位int对于每像素有很多样本的高分辨率图像来说可能不够。

void ImageTileIntegrator::Render() {
	// Declare common variables for rendering image in tiles
    ThreadLocal<ScratchBuffer> scratchBuffers([]() { return ScratchBuffer(); });
    ThreadLocal<Sampler> samplers([this]() { return samplerPrototype.Clone(); });
    
	Bounds2i pixelBounds = camera.GetFilm().PixelBounds();
    int spp = samplerPrototype.SamplesPerPixel();
    ProgressReporter progress(int64_t(spp) * pixelBounds.Area(), "Rendering", Options->quiet);
}

下式中,当前循环(wave)中采样的范围由waveStart和waveEnd给出;nextWaveSize给出下一循环要采集的样本数量。

void ImageTileIntegrator::Render() {
	// Declare common variables for rendering image in tiles
    ThreadLocal<ScratchBuffer> scratchBuffers([]() { return ScratchBuffer(); });
    ThreadLocal<Sampler> samplers([this]() { return samplerPrototype.Clone(); });
    // 显示渲染进度
	Bounds2i pixelBounds = camera.GetFilm().PixelBounds();
    int spp = samplerPrototype.SamplesPerPixel();
    ProgressReporter progress(int64_t(spp) * pixelBounds.Area(), "Rendering", Options->quiet);

	int waveStart = 0, waveEnd = 1, nextWaveSize = 1;
	
	// Render image in waves
    while (waveStart < spp) {
		<<并行渲染当前循环图像块>>
		<<更新开始和结束循环>>
		<<可选地将当前图像写入磁盘>>
	}
}

有了这些变量,渲染将继续进行,直到在所有像素中获得所需数量的样本。

ParallelFor2D()函数在图像分块上循环,并发地运行多次循环迭代;它是B.6节介绍的与并行性相关的实用函数的一部分。循环体由c++ lambda表达式提供。ParallelFor2D()会自动选择一个分块大小,以平衡两个问题:一方面,我们希望分块的数量比系统中的处理器数量多得多;很可能一些块比其他块需要更少的处理时间,因此,如果处理器和块之间存在例如1:1的映射,那么一些处理器在完成它们的工作后将处于空闲状态,而其他处理器将继续处理图像的区域。(图1 - 17展示了渲染一个示例块的平铺图所花费的时间分布,说明了这个问题。)另一方面,过多的块也会影响效率。线程在并行for循环中获得更多工作时,会有一个小的固定开销,并且块(瓦片 tiles)越多,必须支付的开销就越多。因此,ParallelFor2D()选择的分块大小既要考虑待处理区域的范围,也要考虑系统中处理器的数量。
图1-17图1-17

void ImageTileIntegrator::Render() {
	// Declare common variables for rendering image in tiles
    ThreadLocal<ScratchBuffer> scratchBuffers([]() { return ScratchBuffer(); });
    ThreadLocal<Sampler> samplers([this]() { return samplerPrototype.Clone(); });
    // 显示渲染进度
	Bounds2i pixelBounds = camera.GetFilm().PixelBounds();
    int spp = samplerPrototype.SamplesPerPixel();
    ProgressReporter progress(int64_t(spp) * pixelBounds.Area(), "Rendering", Options->quiet);

	int waveStart = 0, waveEnd = 1, nextWaveSize = 1;
	
	// Render image in waves
    while (waveStart < spp) {
		//<<并行渲染当前循环图像块>>
		ParallelFor2D(pixelBounds, [&](Bounds2i tileBounds) {
			<<渲染由tileBounds给出的图像贴图>> 
		}
		<<更新开始和结束循环>>
		<<可选地将当前图像写入磁盘>>
	}
}

给定一个要渲染的块,实现首先为当前正在执行的线程获取ScratchBuffer和Sampler。如前所述,ThreadLocal::Get()方法负责为每个线程分配和返回单个对象的细节。

void ImageTileIntegrator::Render() {
	// Declare common variables for rendering image in tiles
    ThreadLocal<ScratchBuffer> scratchBuffers([]() { return ScratchBuffer(); });
    ThreadLocal<Sampler> samplers([this]() { return samplerPrototype.Clone(); });
    // 显示渲染进度
	Bounds2i pixelBounds = camera.GetFilm().PixelBounds();
    int spp = samplerPrototype.SamplesPerPixel();
    ProgressReporter progress(int64_t(spp) * pixelBounds.Area(), "Rendering", Options->quiet);

	int waveStart = 0, waveEnd = 1, nextWaveSize = 1;
	
	// Render image in waves
    while (waveStart < spp) {
		//<<并行渲染当前循环图像块>>
		ParallelFor2D(pixelBounds, [&](Bounds2i tileBounds) {
			//<<渲染由tileBounds给出的图像贴图>> 
			ScratchBuffer &scratchBuffer = scratchBuffers.Get();//暂用Buffer
            Sampler &sampler = samplers.Get();
            for (Point2i pPixel : tileBounds) {
				<<以像素pPixel渲染样本>>
			}
			progress.Update((waveEnd - waveStart) * tileBounds.Area());
		}
		<<更新开始和结束循环>>
		<<可选地将当前图像写入磁盘>>
	}
}

给定一个要接受一个或多个样本的像素,线程的采样器会通知它应该开始通过StartPixelSample()为当前像素生成样本,这允许它设置任何内部状态,以依赖于当前正在处理的像素。然后,积分器的EvaluatePixelSample()方法负责确定指定样本的值,然后调用ScratchBuffer::Reset()释放它在ScratchBuffer中分配的临时内存。

void ImageTileIntegrator::Render() {
	// Declare common variables for rendering image in tiles
    ThreadLocal<ScratchBuffer> scratchBuffers([]() { return ScratchBuffer(); });
    ThreadLocal<Sampler> samplers([this]() { return samplerPrototype.Clone(); });
    // 显示渲染进度
	Bounds2i pixelBounds = camera.GetFilm().PixelBounds();
    int spp = samplerPrototype.SamplesPerPixel();
    ProgressReporter progress(int64_t(spp) * pixelBounds.Area(), "Rendering", Options->quiet);

	int waveStart = 0, waveEnd = 1, nextWaveSize = 1;
	
	// Render image in waves
    while (waveStart < spp) {
		//<<并行渲染当前循环图像块>>
		ParallelFor2D(pixelBounds, [&](Bounds2i tileBounds) {
			//<<渲染由tileBounds给出的图像贴图>> 
			ScratchBuffer &scratchBuffer = scratchBuffers.Get();//暂用Buffer
            Sampler &sampler = samplers.Get();
            for (Point2i pPixel : tileBounds) {
				//<<以像素pPixel渲染样本>>
				for (int sampleIndex = waveStart; sampleIndex < waveEnd; ++sampleIndex) {
                    // 为当前像素生成样本
                    sampler.StartPixelSample(pPixel, sampleIndex);
                    // 负责确定指定样本的值
                    EvaluatePixelSample(pPixel, sampleIndex, sampler, scratchBuffer);
                    // 释放它在ScratchBuffer中分配的临时内存。
                    scratchBuffer.Reset();
                }
			}
			progress.Update((waveEnd - waveStart) * tileBounds.Area());
		}
		<<更新开始和结束循环>>
		<<可选地将当前图像写入磁盘>>
	}
}

提供了纯虚拟Integrator::Render()方法的实现后,ImageTileIntegrator现在对其子类施加了要求,要求它们实现以下EvaluatePixelSample()方法。

// ImageTileIntegrator Definition
class ImageTileIntegrator : public Integrator {
  public:
    // ImageTileIntegrator Public Methods
    ImageTileIntegrator(Camera camera, Sampler sampler, Primitive aggregate,std::vector<Light> lights)
        : Integrator(aggregate, lights), camera(camera), samplerPrototype(sampler) {}

    void Render();

    virtual void EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler,
                                     ScratchBuffer &scratchBuffer) = 0;

  protected:
    // ImageTileIntegrator Protected Members
    Camera camera;
    Sampler samplerPrototype;
};

在当前循环(wave)的并行for循环完成后,计算在下一循环(wave)中要处理的采样指标的范围。

void ImageTileIntegrator::Render() {
	// Declare common variables for rendering image in tiles
    ThreadLocal<ScratchBuffer> scratchBuffers([]() { return ScratchBuffer(); });
    ThreadLocal<Sampler> samplers([this]() { return samplerPrototype.Clone(); });
    // 显示渲染进度
	Bounds2i pixelBounds = camera.GetFilm().PixelBounds();
    int spp = samplerPrototype.SamplesPerPixel();
    ProgressReporter progress(int64_t(spp) * pixelBounds.Area(), "Rendering", Options->quiet);

	int waveStart = 0, waveEnd = 1, nextWaveSize = 1;
	
	// Render image in waves
    while (waveStart < spp) {
		//<<并行渲染当前循环图像块>>
		ParallelFor2D(pixelBounds, [&](Bounds2i tileBounds) {
			//<<渲染由tileBounds给出的图像贴图>> 
			ScratchBuffer &scratchBuffer = scratchBuffers.Get();//暂用Buffer
            Sampler &sampler = samplers.Get();
            for (Point2i pPixel : tileBounds) {
				//<<以像素pPixel渲染样本>>
				for (int sampleIndex = waveStart; sampleIndex < waveEnd; ++sampleIndex) {
                    // 为当前像素生成样本
                    sampler.StartPixelSample(pPixel, sampleIndex);
                    // 负责确定指定样本的值
                    EvaluatePixelSample(pPixel, sampleIndex, sampler, scratchBuffer);
                    // 释放它在ScratchBuffer中分配的临时内存。
                    scratchBuffer.Reset();
                }
			}
			progress.Update((waveEnd - waveStart) * tileBounds.Area());
		}
		//<<更新开始和结束循环>>
		waveStart = waveEnd;
        waveEnd = std::min(spp, waveEnd + nextWaveSize);
        nextWaveSize = std::min(2 * nextWaveSize, 64);
       
		<<可选地将当前图像写入磁盘>>
	}
}

如果用户提供了-write-partial-images命令行选项,则在处理下一波示例之前,将正在处理的映像写入磁盘。我们不会在这里包括这个处理片段,<<可选地写入当前映像到磁盘>>。

1.3.5 RayIntegrator实现

正如ImageTileIntegrator集中了将图像分解成小块的积分器相关的功能一样,RayIntegrator为从相机开始跟踪光线路径的积分器提供了常用的功能。所有在第13章和第14章实现的整合器都继承自RayIntegrator。

它的构造函数只是将提供的对象传递给ImageTileIntegrator构造函数。

Li()是RayIntegrator子类必须实现的纯虚方法。它返回在原点规定方向上规定波长取样的入射辐射率(radiance)。

// RayIntegrator Definition
class RayIntegrator : public ImageTileIntegrator {
  public:
    // RayIntegrator Public Methods
    RayIntegrator(Camera camera, Sampler sampler, Primitive aggregate, std::vector<Light> lights)
        : ImageTileIntegrator(camera, sampler, aggregate, lights) {}

    void EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler,
                             ScratchBuffer &scratchBuffer) final;

    virtual SampledSpectrum Li(RayDifferential ray, SampledWavelengths &lambda,
                               Sampler sampler, ScratchBuffer &scratchBuffer,
                               VisibleSurface *visibleSurface) const = 0;
};

RayIntegrator实现了ImageTileIntegrator的纯虚函数EvaluatePixelSample()。在给定的像素处,它使用相机和采样器生成一条进入场景的光线,然后调用子类提供的Li()方法来确定沿这条光线到达图像平面的光线数量。在接下来的几章中我们会看到,这个方法返回的值的单位与光线原点入射的光谱辐射度有关,通常用方程中的 L i L_i Li 符号表示,这就是这个方法的名称。这个值传递给 Film 类,记录光线对图像的贡献。

图1.18总结了该方法中使用的主要类以及它们之间的数据流。
图1.18
图1 - 18 RayIntegrator::EvaluatePixelSample()计算的类关系。Sampler为每个要采集的图像样本提供样本值。 Camera将样本转换为来自胶片平面的对应射线,Li()方法计算到达胶片的射线的辐射度。样本及其辐射度被传递到Film,Film将它们的贡献存储在图像中。

// RayIntegrator Method Definitions
void RayIntegrator::EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler, ScratchBuffer &scratchBuffer) {
	<<射线的样本波长>> 
    <<初始化当前样本的CameraSample>> 
    <<为当前样本生成相机光线>> 
    <<如果有效,跟踪camerarray>> 
    <<添加相机光线对图像的贡献>> 
}

每条射线都带有若干离散波长(默认为四种)的辐射 λ \lambda λ 。在计算每个像素的颜色时,pbrt在不同的像素样本上选择不同的波长,使最终结果更好地反映所有波长上的正确结果。为了选择这些波长,采样器首先提供一个采样值 l u lu lu 。该值将均匀分布并在 [ 0 , 1 ) [0,1) [0,1) 范围内。然后 Film:: samplewaves()方法将这个样本映射到一组特定的波长,考虑到它的胶片传感器响应作为波长的函数模型。大多数采样器的实现确保如果在一个像素中获取多个样本,这些样本在总体上是均匀分布的。反过来,它们确保采样的波长也很好地分布在有效波长范围内,提高了图像质量。

// RayIntegrator Method Definitions
void RayIntegrator::EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler, ScratchBuffer &scratchBuffer) {
	//<<射线的样本波长>> 
	Float lu = sampler.Get1D();
    SampledWavelengths lambda = camera.GetFilm().SampleWavelengths(lu);
    <<初始化当前样本的CameraSample>> 
    <<为当前样本生成相机光线>> 
    <<如果有效,跟踪camerarray>> 
    <<添加相机光线对图像的贡献>> 
}

CameraSample结构记录胶片上相机应该为其产生射线的位置。该位置受到采样器提供的采样位置和重建滤波器的影响,重建滤波器用于将多个采样值过滤为像素的单个值。GetCameraSample()处理这些计算。CameraSample还存储与光线和镜头位置样本相关联的时间,分别用于具有移动物体的渲染场景和模拟非针孔孔径的相机模型。

// RayIntegrator Method Definitions
void RayIntegrator::EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler, ScratchBuffer &scratchBuffer) {
	//<<射线的样本波长>> 
	Float lu = sampler.Get1D();
    SampledWavelengths lambda = camera.GetFilm().SampleWavelengths(lu);
    //<<初始化当前样本的CameraSample>> 
    Filter filter = camera.GetFilm().GetFilter();
    CameraSample cameraSample = GetCameraSample(sampler, pPixel, filter);
    <<为当前样本生成相机光线>> 
    <<如果有效,跟踪camerarray>> 
    <<添加相机光线对图像的贡献>> 
}

Camera接口提供了两个生成光线的方法:GenerateRay()和GenerateRayDifferential(),前者返回给定图像样本位置的光线,后者返回光线微分(ray differential),它包含了关于相机将产生的光线的信息,这些光线在图像平面上的xy方向上都是一个像素。在第10章中定义的一些纹理函数中,光线差可以用来计算纹理相对于像素间距的变化速度,从而获得更好的效果,这是纹理反走样的关键组成部分。

对于给定的相机,有些CameraSample值可能与给定相机的有效射线不一致。因此,pstd::optional用于相机返回的CameraRayDifferential。

// RayIntegrator Method Definitions
void RayIntegrator::EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler, ScratchBuffer &scratchBuffer) {
	//<<射线的样本波长>> 
	Float lu = sampler.Get1D();
    SampledWavelengths lambda = camera.GetFilm().SampleWavelengths(lu);
    //<<初始化当前样本的CameraSample>> 
    Filter filter = camera.GetFilm().GetFilter();
    CameraSample cameraSample = GetCameraSample(sampler, pPixel, filter);
    //<<为当前样本生成相机光线>> 
    pstd::optional<CameraRayDifferential> cameraRay = camera.GenerateRayDifferential(cameraSample, lambda);
    <<如果有效,跟踪camerarray>> 
    <<添加相机光线对图像的贡献>> 
}

如果相机光线是有效的,那么在一些额外的准备之后,它将被传递给RayIntegrator子类的 L i ( ) Li() Li() 方法实现。除了沿着光线 L L L 返回亮度之外,子类还负责初始化一个VisibleSurface类的实例,它记录关于光线在每个像素相交(如果有的话)的表面的几何信息,以便使用像GBufferFilm这样的Film 实现,在每个像素存储比颜色更多的信息。

// RayIntegrator Method Definitions
void RayIntegrator::EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler, ScratchBuffer &scratchBuffer) {
	//<<射线的样本波长>> 
	Float lu = sampler.Get1D();
    SampledWavelengths lambda = camera.GetFilm().SampleWavelengths(lu);
    //<<初始化当前样本的CameraSample>> 
    Filter filter = camera.GetFilm().GetFilter();
    CameraSample cameraSample = GetCameraSample(sampler, pPixel, filter);
    //<<为当前样本生成相机光线>> 
    pstd::optional<CameraRayDifferential> cameraRay = camera.GenerateRayDifferential(cameraSample, lambda);
    //<<如果有效,跟踪camerarray>> 
    SampledSpectrum L(0.);
    VisibleSurface visibleSurface;
    if (cameraRay) {
		<<基于图像采样率的相机射线差缩放>> 
	    <<计算沿相机射线的亮度>> 
	    <<如果返回意外的亮度值,则发出警告>> 
	}
    <<添加相机光线对图像的贡献>> 
}

在将射线传递给Li()方法之前,ScaleDifferentials()方法对微分射线进行缩放,以在每个像素取多个样本时考虑胶片平面上样本之间的实际间距。

// RayIntegrator Method Definitions
void RayIntegrator::EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler, ScratchBuffer &scratchBuffer) {
	//<<射线的样本波长>> 
	Float lu = sampler.Get1D();
    SampledWavelengths lambda = camera.GetFilm().SampleWavelengths(lu);
    //<<初始化当前样本的CameraSample>> 
    Filter filter = camera.GetFilm().GetFilter();
    CameraSample cameraSample = GetCameraSample(sampler, pPixel, filter);
    //<<为当前样本生成相机光线>> 
    pstd::optional<CameraRayDifferential> cameraRay = camera.GenerateRayDifferential(cameraSample, lambda);
    //<<如果有效,跟踪camerarray>> 
    SampledSpectrum L(0.);
    VisibleSurface visibleSurface;
    if (cameraRay) {
		//<<基于图像采样率的相机射线差缩放>> 
		Float rayDiffScale = std::max<Float>(.125f, 1 / std::sqrt((Float)sampler.SamplesPerPixel()));
	    <<计算沿相机射线的亮度>> 
	    <<如果返回意外的亮度值,则发出警告>> 
	}
    <<添加相机光线对图像的贡献>> 
}

对于不存储每个像素几何信息的Film实现,值得节省填充 VisibleSurface类的工作。因此,只有在必要时,才会在调用Li()方法时传递指向该类的指针,否则传递null指针。Integrator (积分器)的实现应该只在 VisibleSurface是非空的情况下初始化它。

CameraRayDifferential还带有用于缩放返回的辐射度值的射线相关的权重。对于简单的相机模型,每条射线的权重是相等的,但是更准确地模拟透镜系统成像过程的相机模型可能会产生一些比其他射线贡献更大的射线。这样的相机模型可以模拟到达胶片平面边缘的光比到达胶片中心的光少的效果,这种效果称为光晕(vignetting)。

// RayIntegrator Method Definitions
void RayIntegrator::EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler, ScratchBuffer &scratchBuffer) {
	//<<射线的样本波长>> 
	Float lu = sampler.Get1D();
    SampledWavelengths lambda = camera.GetFilm().SampleWavelengths(lu);
    //<<初始化当前样本的CameraSample>> 
    Filter filter = camera.GetFilm().GetFilter();
    CameraSample cameraSample = GetCameraSample(sampler, pPixel, filter);
    //<<为当前样本生成相机光线>> 
    pstd::optional<CameraRayDifferential> cameraRay = camera.GenerateRayDifferential(cameraSample, lambda);
    //<<如果有效,跟踪camerarray>> 
    SampledSpectrum L(0.);
    VisibleSurface visibleSurface;
    if (cameraRay) {
		//<<基于图像采样率的相机射线差缩放>> 
		Float rayDiffScale = std::max<Float>(.125f, 1 / std::sqrt((Float)sampler.SamplesPerPixel()));
	    // <<计算沿相机射线的亮度>> 
	    // Evaluate radiance along camera ray
        bool initializeVisibleSurface = camera.GetFilm().UsesVisibleSurface();
        L = cameraRay->weight * Li(cameraRay->ray, lambda, sampler, scratchBuffer,
                                   initializeVisibleSurface ? &visibleSurface : nullptr);
	    <<如果返回意外的亮度值,则发出警告>> 
	}
    <<添加相机光线对图像的贡献>> 
}

渲染过程中bug的一个常见副作用是计算不可能的亮度值。例如,除以零的结果是亮度值等于IEEE浮点无穷大或“非数字”值。渲染器会寻找这些可能性,并在遇到它们时打印错误消息。在这里,我们将不包含执行此操作的片段,如果返回意外的辐射值,则发出警告。如果您对其细节感兴趣,请参阅cpu/integrator.cpp中的实现。

在到达射线原点的辐射度已知后,调用Film::AddSample()会根据样本的加权辐射度更新图像中对应的像素。5.4节和8.8节将详细解释如何在影片中记录样本值。

// RayIntegrator Method Definitions
void RayIntegrator::EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler, ScratchBuffer &scratchBuffer) {
	//<<射线的样本波长>> 
	Float lu = sampler.Get1D();
    SampledWavelengths lambda = camera.GetFilm().SampleWavelengths(lu);
    //<<初始化当前样本的CameraSample>> 
    Filter filter = camera.GetFilm().GetFilter();
    CameraSample cameraSample = GetCameraSample(sampler, pPixel, filter);
    //<<为当前样本生成相机光线>> 
    pstd::optional<CameraRayDifferential> cameraRay = camera.GenerateRayDifferential(cameraSample, lambda);
    //<<如果有效,跟踪camerarray>> 
    SampledSpectrum L(0.);
    VisibleSurface visibleSurface;
    if (cameraRay) {
		//<<基于图像采样率的相机射线差缩放>> 
		Float rayDiffScale = std::max<Float>(.125f, 1 / std::sqrt((Float)sampler.SamplesPerPixel()));
	    // <<计算沿相机射线的亮度>> 
	    // Evaluate radiance along camera ray
        bool initializeVisibleSurface = camera.GetFilm().UsesVisibleSurface();
        L = cameraRay->weight * Li(cameraRay->ray, lambda, sampler, scratchBuffer,
                                   initializeVisibleSurface ? &visibleSurface : nullptr);
	    <<如果返回意外的亮度值,则发出警告>> 
	}
    //<<添加相机光线对图像的贡献>> 
    camera.GetFilm().AddSample(pPixel, L, lambda, &visibleSurface, cameraSample.filterWeight);
}

1.3.6随机步进积分器(Random Walk Integrator)

虽然我们花了几页的时间来完成integrator基础架构的实现,最终实现了RayIntegrator,但我们现在可以转向在一个更简单的上下文中实现光传输集成算法,而不是必须开始实现一个完整的integrator::Render()方法。我们将在本节中描述的RandomWalkIntegrator继承自RayIntegrator,因此多线程的所有细节(从相机生成初始光线,然后将光线添加到图像上的辐射度)都得到了处理。积分器在更简单上下文中工作:提供了一条射线,它的任务是计算到达原点的辐射度。

回想一下,在1.2.7节中我们提到,在没有参与介质的情况下,光线所携带的光在通过自由空间时是不变的。我们将忽略在这个积分器的实现中参与媒体的可能性,这允许我们迈出第一步:获取射线与场景中的几何图形的第一个交点,到达射线原点的辐亮度等于离开交点指向射线原点的辐亮度。该出射辐射度由光传输方程(1.1)给出,尽管无法以闭合形式对其进行评估。需要数值计算方法,而pbrt中使用的方法是基于蒙特卡罗积分的,这使得根据被积函数的逐点计算来估计积分的值成为可能。第2章介绍蒙特卡罗积分,并介绍本书将用到的其他蒙特卡罗技术。

为了计算出射的辐射度,RandomWalkIntegrator实现了一种基于增量构建随机步进(random walk)的简单蒙特卡洛方法,在该方法中,场景表面上的一系列点依次随机选择,以构建从相机开始的光传输路径。这种方法有效地反向模拟了现实世界中的图像形成,从相机而不是从光源开始。在这方面,倒退仍然是物理上有效的,因为pbrt所基于的光的物理模型是时间可逆的。

Figure 1.19: A View of the Watercolor Scene, Rendered with the
RandomWalkIntegrator.

Figure 1.19: A View of the Watercolor Scene, Rendered with the
RandomWalkIntegrator.

RandomWalkIntegrator不能处理完美的镜面,所以桌子上的两个玻璃杯是黑色的。此外,即使使用每像素8192个样本来渲染这张图像,结果仍然充斥着高频噪声。(注意,例如,远处的墙壁和椅子的底座。)(场景由Angelo Ferretti提供。)

虽然随机步进采样算法的实现总共只有20多行代码,但它能够模拟复杂的光照和阴影效果;图1 - 19展示了使用它渲染的图像。(然而,这幅图像需要数小时的计算才能达到那种质量水平。)在本节的其余部分,我们将简要介绍积分器实现的一些数学细节,并专注于对该方法的直观理解,随后的章节将填补空白,更严格地解释这一技术以及更复杂的技术。

class RandomWalkIntegrator : public RayIntegrator {
  public:
    <<RandomWalkIntegrator Public Methods>> 
  private:
    <<RandomWalkIntegrator Private Methods>> 
    <<RandomWalkIntegrator Private Members>> 
};

这个积分器递归地计算随机步进。因此,它的Li()方法实现只是通过调用LiRandomWalk()方法来启动递归。虽然这个简单的积分器忽略了VisibleSurface,并添加了一个用于跟踪递归深度的额外参数,但Li()的大多数参数都直接传递了过去。

class RandomWalkIntegrator : public RayIntegrator {
  public:
    //<<RandomWalkIntegrator Public Methods>> 
    SampledSpectrum Li(
   			RayDifferential ray, 
   			SampledWavelengths &lambda,
       		Sampler sampler, 
       		ScratchBuffer &scratchBuffer,
       		VisibleSurface *visibleSurface
       	) const 
    {
	    return LiRandomWalk(ray, lambda, sampler, scratchBuffer, 0);
	}
  private:
    //<<RandomWalkIntegrator Private Methods>> 
    SampledSpectrum LiRandomWalk(
	    	RayDifferential ray,
		    SampledWavelengths &lambda, 
		    Sampler sampler,
	        ScratchBuffer &scratchBuffer, 
	        int depth) const 
	{
	    <<Intersect ray with scene and return if no intersection>> 
	    <<Get emitted radiance at surface intersection>> 
	    <<Terminate random walk if maximum depth has been reached>> 
	    <<Compute BSDF at random walk intersection point>> 
	    <<Randomly sample direction leaving surface for random walk>> 
	    <<Evaluate BSDF at surface for sampled direction>> 
	    <<Recursively trace ray to estimate incident radiance at surface>> 
	}


    <<RandomWalkIntegrator Private Members>> 
};

第一步是找到光线与场景中形状最近的交点。如果没有找到交叉点,则射线已经离开了场景。否则,作为ShapeIntersection结构的一部分返回的SurfaceInteraction会提供交点的局部几何属性信息。

如果没有发现交集,辐射度仍然会被光源(例如ImageInfiniteLight)沿着光路带入,它们与几何无关。Light::Le()方法允许这样的灯返回规定光线的亮度。

SampledSpectrum LiRandomWalk(
	    	RayDifferential ray,
		    SampledWavelengths &lambda, 
		    Sampler sampler,
	        ScratchBuffer &scratchBuffer, 
	        int depth) const 
	{
	    //射线与场景相交,如果没有相交则返回 
	    pstd::optional<ShapeIntersection> si = Intersect(ray);
		if (!si) {
		    //从无限光源返回发出的光
		    SampledSpectrum Le(0.f);
			for (Light light : infiniteLights)
			    Le += light.Le(ray, lambda);
			return Le;
		}
		SurfaceInteraction &isect = si->intr;
		
	    <<Get emitted radiance at surface intersection>> 
	    <<Terminate random walk if maximum depth has been reached>> 
	    <<Compute BSDF at random walk intersection point>> 
	    <<Randomly sample direction leaving surface for random walk>> 
	    <<Evaluate BSDF at surface for sampled direction>> 
	    <<Recursively trace ray to estimate incident radiance at surface>> 
	}

如果找到了一个有效的交点,则必须计算该交点处的光传输方程。第一项 L e ( p , ω o ) L_e(p,\omega_o) Le(p,ωo) ,即射出的辐射度,很简单:发射(emission)是场景规范的一部分,通过调用SurfaceInteraction::Le()方法可以获得发射的辐射度,该方法取关注的出方向。在这里,我们感兴趣的是沿射线方向发射回来的辐射。如果目标不是发射的,该方法返回一个零值光谱分布。

SampledSpectrum LiRandomWalk(
	    	RayDifferential ray,
		    SampledWavelengths &lambda, 
		    Sampler sampler,
	        ScratchBuffer &scratchBuffer, 
	        int depth) const 
	{
	    //射线与场景相交,如果没有相交则返回 
	    pstd::optional<ShapeIntersection> si = Intersect(ray);
		if (!si) {
		    //从无限光源返回发出的光
		    SampledSpectrum Le(0.f);
			for (Light light : infiniteLights)
			    Le += light.Le(ray, lambda);
			return Le;
		}
		SurfaceInteraction &isect = si->intr;
		
	    // 得到在表面交叉处发射的辐射亮度
        Vector3f wo = -ray.d;
        SampledSpectrum Le = isect.Le(wo, lambda);
        
	    <<Terminate random walk if maximum depth has been reached>> 
	    <<Compute BSDF at random walk intersection point>> 
	    <<Randomly sample direction leaving surface for random walk>> 
	    <<Evaluate BSDF at surface for sampled direction>> 
	    <<Recursively trace ray to estimate incident radiance at surface>> 
	}

评估第二项的光输运方程需要计算积分球的方向的交点 p p p 。应用蒙特卡罗积分的原则可以用来表示:如果方向 w ′ w ' w 选择以同样的概率在所有可能的方向,积分的估计可以用 B S D F f BSDF f BSDFf的加权乘积来计算,它用来描述材料的光散射 p p p ,入射光照、和余弦因子的属性:
在这里插入图片描述

换句话说,给定一个随机的方向 w ′ w' w,估计积分值需要计算被积函数中那个方向的项,然后乘以4π。(这个因子,在a .5.2节中推导出来,与单位球体的表面积有关。)由于只考虑一个方向,因此蒙特卡罗估计与积分的真实值相比几乎总是存在误差。然而,可以证明像这样的估计在期望上是正确的:非正式地说,它们平均给出了正确的结果。因此,每个像素取多个样本,平均多个独立估计通常会减少这种误差。

估计的BSDF和余弦因子很容易计算,但是入射辐射度 L i L_i Li是未知的。然而,请注意,我们发现自己又回到了第一次调用LiRandomWalk()时的位置:我们有一条光线,我们希望找到它在原点的入射辐射度——递归调用LiRandomWalk()将提供它。

在计算积分的估计值之前,必须考虑终止递归。RandomWalkIntegrator会在预定的最大深度maxDepth处停止。如果没有这个终止标准,算法可能永远不会终止(想象一下,例如,一个满是镜子的场景)。这个成员变量在构造函数中初始化,参数可以在场景描述文件中设置。

class RandomWalkIntegrator : public RayIntegrator {
  public:
    //RandomWalkIntegrator Public Methods
    SampledSpectrum Li(...) const {
	    return LiRandomWalk(ray, lambda, sampler, scratchBuffer, 0);
	}
  private:
    //RandomWalkIntegrator Private Methods
    SampledSpectrum LiRandomWalk(...){}
    //RandomWalkIntegrator Private Members
    int maxDepth;
};
SampledSpectrum LiRandomWalk(
	    	RayDifferential ray,
		    SampledWavelengths &lambda, 
		    Sampler sampler,
	        ScratchBuffer &scratchBuffer, 
	        int depth) const 
	{
	    //射线与场景相交,如果没有相交则返回 
	    pstd::optional<ShapeIntersection> si = Intersect(ray);
		if (!si) {
		    //从无限光源返回发出的光
		    SampledSpectrum Le(0.f);
			for (Light light : infiniteLights)
			    Le += light.Le(ray, lambda);
			return Le;
		}
		SurfaceInteraction &isect = si->intr;
		
	    // 得到在表面交叉处发射的辐射亮度
        Vector3f wo = -ray.d;
        SampledSpectrum Le = isect.Le(wo, lambda);
        
	    //如果达到最大深度,终止随机漫步
		if (depth == maxDepth)
            return Le;
 
	    <<Compute BSDF at random walk intersection point>> 
	    <<Randomly sample direction leaving surface for random walk>> 
	    <<Evaluate BSDF at surface for sampled direction>> 
	    <<Recursively trace ray to estimate incident radiance at surface>> 
	}

如果随机步进没有结束,则调用SurfaceInteraction::GetBSDF()方法来查找交叉点处的BSDF。它评估纹理函数以确定表面属性,然后初始化BSDF的表示。它通常需要为构成BSDF表示的对象分配内存;因为这个内存只需要在处理当前光线时激活,所以我们为它提供了一个ScratchBuffer来分配它。

SampledSpectrum LiRandomWalk(
	    	RayDifferential ray,
		    SampledWavelengths &lambda, 
		    Sampler sampler,
	        ScratchBuffer &scratchBuffer, 
	        int depth) const 
	{
	    //射线与场景相交,如果没有相交则返回 
	    pstd::optional<ShapeIntersection> si = Intersect(ray);
		if (!si) {
		    //从无限光源返回发出的光
		    SampledSpectrum Le(0.f);
			for (Light light : infiniteLights)
			    Le += light.Le(ray, lambda);
			return Le;
		}
		SurfaceInteraction &isect = si->intr;
		
	    // 得到在表面交叉处发射的辐射亮度
        Vector3f wo = -ray.d;
        SampledSpectrum Le = isect.Le(wo, lambda);
        
	    //如果达到最大深度,终止随机漫步
		if (depth == maxDepth)
            return Le;
 
	    //计算随机步进交点的BSDF
        BSDF bsdf = isect.GetBSDF(ray, lambda, camera, scratchBuffer, sampler);
        
	    <<Randomly sample direction leaving surface for random walk>> 
	    <<Evaluate BSDF at surface for sampled direction>> 
	    <<Recursively trace ray to estimate incident radiance at surface>> 
	}

接下来,我们需要随机采样一个方向 ω ′ \omega' ω来计算公式(1.2)中的估计。给定采样器提供的两个 [ 0 , 1 ) [0,1) [0,1)均匀分布的值,SampleUniformSphere()函数返回单位球面上均匀分布的方向。

SampledSpectrum LiRandomWalk(
	    	RayDifferential ray,
		    SampledWavelengths &lambda, 
		    Sampler sampler,
	        ScratchBuffer &scratchBuffer, 
	        int depth) const 
	{
	    //射线与场景相交,如果没有相交则返回 
	    pstd::optional<ShapeIntersection> si = Intersect(ray);
		if (!si) {
		    //从无限光源返回发出的光
		    SampledSpectrum Le(0.f);
			for (Light light : infiniteLights)
			    Le += light.Le(ray, lambda);
			return Le;
		}
		SurfaceInteraction &isect = si->intr;
		
	    // 得到在表面交叉处发射的辐射亮度
        Vector3f wo = -ray.d;
        SampledSpectrum Le = isect.Le(wo, lambda);
        
	    //如果达到最大深度,终止随机漫步
		if (depth == maxDepth)
            return Le;
 
	    //计算随机步进交点的BSDF
        BSDF bsdf = isect.GetBSDF(ray, lambda, camera, scratchBuffer, sampler);
        
	    // 在离开曲面的方向上随机采样,进行随机步进
	    Point2f u = sampler.Get2D();
        Vector3f wp = SampleUniformSphere(u);

	    <<Evaluate BSDF at surface for sampled direction>> 
	    <<Recursively trace ray to estimate incident radiance at surface>> 
	}

除了入射辐射度之外,蒙特卡罗估计的所有因素现在都可以很容易地计算出来。BSDF类提供了一个f()方法,可以计算给定两个方向下的BSDF,而角度与表面法向量夹角的余弦值可以使用AbsDot()函数计算,该函数返回两个向量点积的绝对值。如果向量被归一化,就像这里的两个向量一样,这个值等于它们之间夹角的余弦的绝对值(第3.3.2节)。

在给定的方向上,BSDF可能是零值,因此fcos也可能是零值——例如,如果表面不是透射的,但两个方向在它的相反两侧,BSDF是零。在这种情况下,没有理由继续随机漫步,因为后续的点对结果没有贡献。

SampledSpectrum LiRandomWalk(
	    	RayDifferential ray,
		    SampledWavelengths &lambda, 
		    Sampler sampler,
	        ScratchBuffer &scratchBuffer, 
	        int depth) const 
	{
	    //射线与场景相交,如果没有相交则返回 
	    pstd::optional<ShapeIntersection> si = Intersect(ray);
		if (!si) {
		    //从无限光源返回发出的光
		    SampledSpectrum Le(0.f);
			for (Light light : infiniteLights)
			    Le += light.Le(ray, lambda);
			return Le;
		}
		SurfaceInteraction &isect = si->intr;
		
	    // 得到在表面交叉处发射的辐射亮度
        Vector3f wo = -ray.d;
        SampledSpectrum Le = isect.Le(wo, lambda);
        
	    //如果达到最大深度,终止随机漫步
		if (depth == maxDepth)
            return Le;
 
	    //计算随机步进交点的BSDF
        BSDF bsdf = isect.GetBSDF(ray, lambda, camera, scratchBuffer, sampler);
        if (!bsdf)
            return Le;
        
	    // 在离开曲面的方向上随机采样,进行随机步进
	    Point2f u = sampler.Get2D();
        Vector3f wp = SampleUniformSphere(u);

	    //计算采样方向表面的BSDF
	    SampledSpectrum fcos = bsdf.f(wo, wp) * AbsDot(wp, isect.shading.n);
		if (!fcos)
		    return Le;
	    <<Recursively trace ray to estimate incident radiance at surface>> 
	}

剩下的任务是计算在采样方向 ω ′ \omega' ω上离开表面的新射线。这个任务由SpawnRay()方法完成,该方法返回一条与给定方向相交的光线,确保光线与表面有足够的偏移,不会因为舍入误差而错误地重新相交。给定光线,可以递归调用LiRandomWalk()来估计入射辐射度,从而完成方程(1.2)的估计。

SampledSpectrum LiRandomWalk(
	    	RayDifferential ray,
		    SampledWavelengths &lambda, 
		    Sampler sampler,
	        ScratchBuffer &scratchBuffer, 
	        int depth) const 
	{
	    //射线与场景相交,如果没有相交则返回 
	    pstd::optional<ShapeIntersection> si = Intersect(ray);
		if (!si) {
		    //从无限光源返回发出的光
		    SampledSpectrum Le(0.f);
			for (Light light : infiniteLights)
			    Le += light.Le(ray, lambda);
			return Le;
		}
		SurfaceInteraction &isect = si->intr;
		
	    // 得到在表面交叉处发射的辐射亮度
        Vector3f wo = -ray.d;
        SampledSpectrum Le = isect.Le(wo, lambda);
        
	    //如果达到最大深度,终止随机漫步
		if (depth == maxDepth)
            return Le;
 
	    //计算随机步进交点的BSDF
        BSDF bsdf = isect.GetBSDF(ray, lambda, camera, scratchBuffer, sampler);
        if (!bsdf)
            return Le;
        
	    // 在离开曲面的方向上随机采样,进行随机步进
	    Point2f u = sampler.Get2D();
        Vector3f wp = SampleUniformSphere(u);

	    //计算采样方向表面的BSDF
	    SampledSpectrum fcos = bsdf.f(wo, wp) * AbsDot(wp, isect.shading.n);
		if (!fcos)
		    return Le;
	    // 递归跟踪射线以估计表面的入射辐射
	    ray = isect.SpawnRay(wp);
        return Le + fcos * LiRandomWalk(ray, lambda, sampler, scratchBuffer, depth + 1) /
                        (1 / (4 * Pi));
	}

这种简单的方法有许多缺点。例如,如果发射面很小,大多数射线路径将找不到任何光线,形成准确的图像需要许多光线跟踪。在点光源的极限情况下,图像将是黑色的,因为与这样的光源相交的概率为零。类似的问题也适用于BSDF模型,它将光线散射到一组集中的方向上。在完美镜子沿单一方向散射入射光的极限情况下,RandomWalkIntegrator永远无法随机对该方向进行采样。

这些问题以及更多的问题可以通过更复杂的蒙特卡罗积分技术的应用来解决。在后续章节中,我们会介绍一系列改进方法,以得到更精确的结果。第13章到第15章中定义的集成器是这些发展的级点。它们仍然基于RandomWalkIntegrator中使用的基本思想,但比RandomWalkIntegrator更高效、更健壮。图1 - 20比较了RandomWalkIntegrator和改进后的integrator,从中可以看出改进的幅度。

在这里插入图片描述
图1.20 (a)
在这里插入图片描述
图1.20 (b)- 水彩场景渲染使用32个样本每像素。

  • (a)使用RandomWalkIntegrator渲染。
  • (b)使用PathIntegrator渲染。

它遵循相同的一般方法,但使用更复杂的蒙特卡洛技术。PathIntegrator为大致相同的工作量提供了更好的图像,并减少了均方误差。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值