PBRT阅读:第七章 采样和重构 第7.2-7.3节

来源:互联网 发布:淘宝打字客服容易吗 编辑:程序博客网 时间:2024/05/07 10:38

http://www.opengpu.org/bbs/forum.php?mod=viewthread&tid=5031&fromuid=10107

7.2 图像采样接口

现在我们可以讨论几个能够生成良好的图像采样模式的类了。这些类背后所隐含的复杂性可能会令人吃惊。实际上,创建良好的采样模式可以对光线追踪程序的效率有实质性的提高,能够使程序跟采用低质量的采样模式相比,使用更少的光线却能生成高质量的图像。采用最好的采样模式所花费的时间跟采用低质量的模式的时间大致相同,但由于求图像采样的辐射亮度的代价很大,所以采用良好的采样模式是非常值得的。

所有采样器的实现都继承了抽象类Sampler,而Sampler定义了它们的共用接口。采样器的任务是生成一个多维采样位置的序列。其中两个维给出了光栅空间的图像采样位置,另一个维给出了采样的时间,时间值的范围是从0到1,并比例变换到相机快门打开的时间周期中。还有两个采样值给出了为了计算景深的镜头位置(u,v);它们的范围也是从0到1。

位置恰到好处的采样点有助于克服二维图像函数的复杂性,同样地,第16章介绍的大多数光传输算法也利用采样点,例如在面光源上选取位置来估算照明度,这也是Sampler的一项工作,因为它在选择新采样点时会考虑到为邻近图像采样的那些采样点。这样做会提高光传输算法的质量。

<Sampling Declarations> =
    class COREDLL Sampler {
    public:
        <Sample Interface>
        <Sample Public Data>
    };

所有Sampler的实现都传给基类构造器几个共同的参数,包括图像的分辨率以及为每个像素所生成的采样个数。

<Sample Method Definitions> =
    Sampler::Sampler(int xstart, int xend, int ystart, int yend, int spp) {
        xPixelStart = xstart;
        xPixelEnd = xend;
        yPixelStart = ystart;
        yPixelEnd = yend;
        SamplesPerPixel = spp;
    }

Sampler为x坐标范围是xPixelStart到xPixelEnd-1以及y坐标范围是yPixelStart到yPixelEnd-1的像素生成采样。

<Sampler Public Data>=
    int xPixelStart, xPixelEnd, yPixelStart, yPixelEnd;
    int samplePerPixel;

采样器必须实现Sampler::GetNextSample()函数,它是一个纯虚函数。Scene::Render()函数调用该函数,直到它返回false值;当该函数返回true时,就把下一个采样值放到sample中。除了imageX和imageY以外,其它所有维的取值范围都在0到1之间。

<Sampler Interface> =
    virtual bool GetNextSample(Sample *sample) = 0;

为了使渲染主循环能够知道场景已经被渲染的百分比,Sampler::TotalSamples()函数返回Sampler将要返回的采样总数。

<Sampler Interface> +=
    int TotalSamples() const {
        return samplesPerPixel *
            (xPixelEnd - xPixelStart) * (yPixelEnd - yPixelStart);
    }

7.2.1 采样的表示和空间申请

采样器用Sample结构来存放一个采样。在Scene::Render()函数中要申请一个Sample对象。对于要生成的每条相机光线,我们把这个Sample的指针传给Sampler进行初始化。然后把这个Sample传给相机和积分器,利用它的值来构造相机光线和进行照明计算。

积分器因采用的光传输算法细节的不同而有不同的采样要求。例如,WhttedIntegrator没有随机采样,也就没有额外的采样值;而DirectLighting积分器利用Sampler中的值随机地选择一个光源进行照明采样,也会随机地在面光源上选择采样。因此,积分器要有机会来提出对不同的量进行额外的采样的请求。这些对采样的请求被存放在Sample对象中。当Sample对象被传给一个特定的Sampler实现时,Sampler负责生成所需类型的采样。

<Sampling Declarations> +=
    struct Sample {
        <Sample Public Methods>
        <Camera Sample Data>
        <Integrator Sample Data>
    };

在Sampler中,相机所使用的数据是固定的,在第6章中,我们已经看到过相机对这些数据的使用情况。

<Camera Sample Data>
    float imageX, imageY;
    float lensU, lensV;
    float time;

Sampler构造器立即调用面积分器和体积分器的Intergrator::RequestSamples()函数来确定如何进行采样。积分器可以请求多个一维和/或二维采样模式,每个模式可以有任意多个采样。例如,在有两个面光源的场景中,积分器追踪到第一个光源的4条阴影光线,也追踪到第二个光源的8条阴影光线,积分器就会请求两个二维采样模式,分别有4个和8个采样。需要二维的模式是因为对光源的参数化需要两个维。类似地,如果积分器想从多个光源中随机地选择一个,就会请求一个带有一个单值的一维模式,并利用它的浮点值来随机地选择一个光源。

积分器尽其所需地向采样器请求随机采样,采样器就可以仔细地构造出覆盖整个高维采样空间的采样点。例如,在计算面光源照明时,如果附近的图像采样倾向于在面光源不同的位置进行采样,就会发掘出更多的信息,最终的图像就会更好些。

在pbrt中,我们不允许积分器请求三维或更高维的采样模式,因为它们对于所实现的渲染算法而言没有什么用处。如果有必要,积分器可以把来自低维模式的点组合起来,生成高维的采样点(例如,一个一维采样模式和相同大小的二维采样模式可以形成一个三维模式)。虽然跟直接生成的三维模式相比有所欠缺,但是在实际应用中还是可以接受的。在绝对必要的情况下,积分器可以自己生成一个三维采样模式。

积分器实现Integrator::RequestSample()函数,这个函数调用Sample::Add1D()和Sample::Add2D()函数,这两个函数需要另一个给定数目的采样序列。调用完毕后,Sample构造器既可以进而为采样值申请内存空间。

<Sample Method Definitions> =
    Sample::Sample(SurfaceIntegrator *surf, VolumeIntegrator *vol, const Scene *scene) {
        surf->RequestSamples(this, scene);
        vol->RequestSamples(this, scene);
        <Allocate storage for sample pointers>
        <Compute total number of sample values needed>
        <Allocate storage for sample values>
    }

Sample::Add1D() 和Sample::Add2D()函数把所请求的采样数记录到一个数组中,返回一个索引值,可供积分器在Sample里存取所需的采样值。

<Sample Public Methods> =
    u_int Add1D(u_int num) {
        n1D.push_back(num);
        return n1D.size() -1;
    }
    u_int Add2D(u_int num) {
        n2D.push_back(num);
        return n2D.size() -1;
    }

大多数采样器更擅长于生成某些特定数目的采样。例如,LDSampler可以生成非常好的模式,但是模式大小必须是2的幂。Sampler::RoundSize()用于提供这类信息。积分器可以把所需采样的数目传给这个函数,使得Sampler有机会相应地使用一个方便的采样数目。积分器应该把返回值作为所需采样的数目。

<Sampler Interface> +=
    virtual int RoundSize(int size) const = 0;

Sampler负责把所生成的采样存贮在Sample:oneD和Sample::twoD两个数组里。对于一维采样模式,Sampler要生成n1D.size()个相互独立的模式,第i个模式有n1D个采样值。这些值分别存放在数组元素oneD[0] 到oneD[n1D-1]中。

<Integrator Sample Data> =
    vector<u_int> n1D, n2D;
    float **oneD, **twoD;

为了存取采样,积分器把从Add1D()返回的采样索引值放在一个成员变量(例如sampleOffset)中,然后就可以按如下方式存取采样值了:

    for (int i = 0; i < sample->n1D[sampleOffset]; ++i) {
        float s = sample->oneD[sampleOffset];
        ...
    }

二维的处理过程跟一维相同,只不过第i个采样是两个数:sample->twoD[offset][2*i]和sample->twoD[offset][2*i+1]。
Sampler构造器要为这些指针申请空间,它一次性地为两个采样数组oneD和twoD申请了足够的空间,而不是分两次申请。只需把twoD设置为正确的偏置值,即oneD的最后一个指针之后。这样的一次性申请可以保证oneD和twoD指向彼此靠近的内存位置,从而可以提高缓存效率。

<Allocate storage for sample pointers> =
    int nPtrs = n1D.size() + n2D.size();
    if (!nPtrs) {
        oneD = twoD = NULL;
        return;
    }
    oneD = (float **)AllocateAligned(nPtrs * sizeof(float *));
    twoD = oneD + n1D.size();

然后我们使用一些技巧来为实际采样值申请连续的内存。首先,先确定所需float的总数:

<Compute total number of sample values needed> =
    int totSamples = 0;
    for(u_int i = 0; i < n1D.size(); ++i)
        totSamples += n1D;
    for(u_int i = 0; i < n2D.size(); ++i)
        totSamples += n2D;

然后,构造器可以申请一块连续的内存,然后对oneD和twoD里的指针赋上相应的内存地址:

<Allocate storage for sample values> =
    float *mem = (float *) AllocAligned(totSamples * sizeof(float));
    for(u_int i = 0; i < n1D.size(); ++i) {
        oneD = mem;
        mem += n1D;
    }
    for(u_int i = 0; i < n2D.size(); ++i) {
        twoD = mem;
        mem += 2 * n2D;
    }

Sampler的析构器就不介绍了,因为它只是释放动态申请的内存而已。

7.3 分层采样

我们要介绍的第一个采样生成器把图像平面分割为矩形区域,并在每个区域中生成一个采样。这些区域通常称为层(strata),这个采样器就被称为分层采样器(Stratified Sampler)。分层的关键思想是把采样区域分为不重叠的区域,并从每个区域中取一个采样,这样我们就有可能丢失图像的重要特征,因为采样无法保证保持足够近的距离。从另一个角度讲,如果许多的采样都来自采样空间中的临近点,这也对我们没有什么好处,因为每个新采样对图像函数而言没有增加什么新的信息。从信号处理的角度来看,我们隐性地定义了总体采样速率:层(strata)越小,则层就越多,采样速率就越高。

分层采样器利用“颤动”(jittering)的方式在每个层中生成采样的随机点:即把层的中心点随机地偏移一个位移,最大不超过层的宽或高的一半。这种颤动所产生的不均匀性把走样转换成了噪声。这个采样器也提供了非颤动模式,即在层中进行均匀采样,这个模式有益于不同的采样技术的比较,但并不用来生成最后的图像。

pbrt-072-01.jpg 

图7.14显示了几种采样模式: (a)是完全随机的采样模式,没有利用分层,结果是很糟糕的采样模式:有些区域采样很少,而有些区域采样又过多。(b)是均匀采样的分层模式,但无法处理走样。(c)是颤动的分层模式,跟(a)相比有更好的采样分布,跟(b)相比又克服了走样。

pbrt-072-02.jpg 

图7.15显示了利用分层采样器所渲染的图像:(a)是一个参考图像,每个像素用了256个采样。(b)在每个像素上使用了一个采样,没有颤动,注意棋盘格边缘上的走样现象。(c) 使用了颤动技术,但在每个像素上仍然使用了一个采样,走样现象被转换为噪声。(d)在每个像素上使用了4个颤动采样,虽然仍比不上参考图像,但效果比(c)要好了许多。

<StratifiedSampler Declarations> =
    class StratifiedSampler : public Sampler {
    public:
        < StratifiedSampler Public Methods>
    private:
        < StratifiedSampler Private Data>
    };

StratifiedSampler从左到右、从上到下地对像素进行循环来生成采样,在每个像素的层(strata)上生成所有的采样之后再处理下一个像素。构造器所需的参数是:生成采样的像素范围[xstart,ystart]到[xend-1,yend-1],x和y方向上的层数xs,ys, 一个指明是否进行颤动采样的布尔值(jitter)。

<StratifiedSampler Method Definitions> =
    StratifiedSampler:: StratifiedSampler(int xstart, int xend,
            int ystart, int yend, int xs, int ys, bool jitter)
        : Sampler(xstart, xend, ystart, yend, xs * ys) {
        jitterSamples = jitter;
        xPos = xPixelStart;
        yPos = yPixelStart;
        xPixelSamples = xs;
        yPixelSamples = ys;
        <Allocate storage for a pixel's worth of stratified samples>
        <Generate stratified camera samples for (xPos, yPos)>
    }

采样器保有当前像素的坐标xPos和yPos,它们被初始化为图像的左上角。注意在裁剪窗口和采样滤波处理的情况下,该值并不一定是(0,0)。

<StratifiedSampler Private Data> = 
    int xPixelSamples, yPixelSamples;
    bool jitterSamples;
    int xPos, yPos;

StratifiedSampler对于积分器而言并没有对采样个数的选定(即RoundSize(size)返回size值本身):

<StratifiedSampler Public Methods> =
    int RoundSize(int size) const {
        return size;
    }

StratifiedSampler一次性地计算出图像、时间和镜头采样,跟单独计算出各个采样值相比,这样做可以计算出具有更好分布模式的时间和镜头采样。因为GetNextSample()函数一次只提供一个采样,这里构造器申请了存储一个像素的所有采样的采样值的内存空间。

<Allocate storage for a pixel's worth of stratified samples> =
    imageSamples = (float *)AllocAligned (5 * xPixelSamples *
                yPixelSamples * sizeof(float));
    lensSamples = imageSamples + 2 * xPixelSamples * yPixelSamples;
    timeSamples = lenSamples + 2 * xPixelSamples * yPixelSamples;

<StratifiedSampler Private Data> +=
    float * imageSamples, * lensSamples, * timeSamples;

不好的多维采样分层会产生数目惊人的采样。例如,如果我们对图像、镜头、时间的五维空间在每个维上分为4个层,则每个像素有45 = 1024个采样。我们可以在某些维上少采样来解决这个问题(或者对某些维不分层,即只有一个层)。但是,这样会在这些维上失去了分层采样的好处。这个关于分层的问题称为“维度的诅咒(curse of dimensionality)”。

我们可以这样做来得到分层带来的好处却没有过多的采样:计算维子集(subset of dimensions)的分层模式,并随机地把这些维子集上的采样联系起来。图7.16显示了这个基本思想:我们想在每个像素上取4个采样,但仍然对所有的维进行分层。我们生成4个分层的2D图像采样,4个分层的1D时间采样,4个分层的2D镜头采样。然后我们随机地把一个时间采样和一个镜头采样与每个图像采样相联系。结果就是每个像素的采样都具有对采样空间的良好的覆盖性。

pbrt-072-03.jpg 

在StratifiedSampler中,我们用这个技术来为当前像素生成相机采样。在GetNextSample()生成新采样的时候,samplePos变量跟踪在图像、时间和镜头采样数组的当前位置。

<Generate stratified camera samples for (xPos, yPos)> =
    StratifiedSample2D(imageSamples, xPixelSamples, yPixelSamples, jitterSamples);
    StratifiedSample2D(lenSamples, xPixelSamples, yPixelSamples, jitterSamples);
    StratifiedSample2D(timeSamples, xPixelSamples, yPixelSamples, jitterSamples);
    <Shift stratified image samples to pixel coordinates>
    <Decorrelate sample dimensions>
    samplePos = 0;

<StratifiedSampler Private Data> +=
    int  samplePos;

我们把1D和2D的分层采样例程实现为工具函数,因为它们在pbrt的其它地方也会被用到。这两个函数只是对[0,1]范围内给定的层进行循环,在每个层上放置一个采样值。

<Sampling Functions Definitions> =
    COREDLL void StratifiedSample1D(float *samp, int nSamples, bool jitter) {
        float invTot = 1.f / nSamples;
        for (int i = 0; i < nSamples; ++i) {
            float j = jitter ? RandomFloat() : 0.5f;        
            *samp++ = (i + j) * invTot;
        }
    }

<Sampling Functions Definitions> +=
    COREDLL void StratifiedSample2D(float *samp,int nx, int ny, bool jitter) {
        float dx = 1.f / nx, dy = 1.f / ny;
        for (int y = 0; y < ny; ++y) 
            for (int x = 0; x < nx; ++x) {
                float jx = jitter ? RandomFloat() : 0.5f;
                float jy = jitter ? RandomFloat() : 0.5f;
                *samp++ = (x + jx) * dx;
                *samp++ = (y + jy) * dy;
            }
    }

StratifiedSample2D()工具函数生成在[0,1]范围之间的采样,但图像采样要用连续的像素坐标来表示。因此,这个函数要对所有新生成的层采样循环,加上(x,y)像素值,这样对离散的(x,y)像素范围而言的采样,它们的连续变化范围是[x, x+1) x [y, y+1),并遵循第7.1.7节所介绍的使用连续像素坐标的约定规则。下图7.18显示了采样和像素的离散和连续坐标的关系:


<Shift stratified image samples to pixel coordinates> =
    for (int o = 0; o < 2 * xPixelSamples * yPixelSamples; o+= 2) {
        imageSamples[o] += xPos;
        imageSamples[o+1] += xPos;
    }

pbrt-072-04.jpg 

为了把一个时间采样和一个镜头采样跟每个图像采样联系起来,Shuffle()函数打乱时间和镜头采样数组的次序。这样一来,当用第i个事先计算好的采样值来初始化一个Sample时,只返回第i个时间和镜头采样即可。

<Decorrelate sample dimensions> =
    Shuffle(lensSample, xPixelSamples * yPixelSamples, 2);
    Shuffle(timeSample, xPixelSamples * yPixelSamples, 1);

Shuffle()工具函数对一个dims维的具有count个采样的采样模式进行随机排列:

<Sampling Functions Definitions> +=
    COREDLL void Shuffle (float *samp, int count, int dims) {
        for (int i = 0; i < count; i++) {
            u_int other  = RandomUInt() % count;
            for (int j = 0; j < dims; ++j)
                swap(samp[dims * i + j], sam[dims * other + j]);
        }
    }

有了上面的基础,就可以实现StratifiedSampler的GetNextSample()函数了。首先,要先检查是否需要为一个新像素生成采样,以及所有的采样都已经生成完毕。然后,如果需要的话,将生成新的采样,并初始化Sample指针。

<StratifiedSampler Method Definitions> +=
    bool StratifiedSampler::GetNextSample(Sample *sample) {
        <Compute new set of samples if needed for next pixel>
        <Return next StratifiedSampler sample point>
        return true;
    }

samplePos变量跟踪采样表的当前位置,当它达到表的尾部,采样器将移到下一个像素。

<Compute new set of samples if needed for next pixel> =
    if (samplePos == xPixelSamples * yPixelSamples) {
        <Advance to next pixel for stratified sampling>
        <Generate stratified camera samples for (xPos, yPos)>
    }

为了前进到下一个像素,首先要试着在x方向上移动一个像素。如果移动到了图像外面,就要重设x位置为下一行像素的第一个像素的x坐标,并将y位置前移。由于yPos只有当x方向上的最后一个像素处理完毕时才前移,所以当它移过图像的底部,整个采样过程就结束,函数返回false值。

<Advance to next pixel for stratified sampling> =
    if (++xPos == xPixelEnd ) {
        xPos = xPixelStart;
        ++yPos;
    }
    if(yPos == yPixelEnd)
        return false;

因为相机采样已经被计算好并存放在采样表里,所以就很容易对Sample的各个成员初始化:

<Return next StratifiedSampler sample point> =
    sample->imageX = imageSamples[2 * samplePos];
    sample->imageY = imageSamples[2 * samplePos+1];
    sample->lensU = lensSamples[2 * samplePos];
    sample->lensV = lensSamples[2 * samplePos+1];
    sample->time = timeSamples[samplePos];
    <Generate stratified samples for integrators>
    ++samplePos;

积分器会带来一定的复杂性,因为积分器经常在某些维上要对每个图像采样使用多个采样,而不像相机采样那样对镜头和时间只需要一个采样值。这就令我们陷入两难境地:如果一个积分器需要为每个图像采样申请64个二维采样值,采样器就要实现两个不同的目标:

1. 我们希望每个图像采样的64个积分器采样有很好的二维分布(即分布在8x8的层网格内)。分层技术会改进积分器在每个采样的结果。

2. 我们还希望能够保证每个图像采样的积分器采样集合跟其相邻的图像采样相比,不要太近似。就像时间采样和镜头采样那样,我们希望采样点能有很好的分布,使得单个像素的区域上有很好的对采样空间上的覆盖。

StratifiedSampler并不同时解决这两个问题,而是只着重解决第一个问题。在介绍本章的其它采样器时,我们再回到这个问题,在某种程度上,我们将用更复杂的技术来同时解决这两个问题。

第二个跟积分器相关的复杂性是:积分器可能会请求为每个图像采样申请任意数目的采样,使得分层很难进行(例如,我们怎么为7个采样生成一个2D分层模式呀?)我们可以仅仅生成nx1或1xn的分层模式,但这只在一维上有益。我们要采用的方法被称为拉丁超立方体采样(Latin hypercube sampling, LHS),它可以生成任意维度上的任何数目的采样,同时有很好的分布。

LHS将每个维上的轴平均分成n等分,在沿着对角线上的n个区域中,在每个区域中生成一个颤动的采样,然后在每个维上对这些采样进行随机换位,这样所产生的模式就具有良好的分布。

pbrt-072-05.jpg 

LHS的一个优点是减少了采样的在某个维上的轴投影所产生的拥挤现象。下图是一个分层采样的一个最差情形:这是一个n x n模式,有2n个采样点几乎投影到一个轴上的同一个点:

pbrt-072-06.jpg 

尽管LHS解决了采样点的拥挤问题,但并一定不对分层采样有所改进,因为很容易构造出这样的情形:许多采样点的位置是共线的,以及[0,1]x[0,1]中的大区域没有采样点。特别地,当n增大时,LHS跟分层采样相比就更没有效率。在下一节里我们在回到这个问题,届时我们会讨论进行分层并同时使用拉丁超立方体分布的采样模式。

<Generate stratified samples for integrators> =
    for (u_int i = 0; i < sample->n1D.size(); ++i)
        LatinHypercube(sample->oneD, sample->n1D, 1);
    for (u_int i = 0; i < sample->n2D.size(); ++i)
        LatinHypercube(sample->twoD, sample->n2D, 2);

通用的LatinHypercube()函数可以在任意维度生成任意数目的LHS采样。sample数组的元素个数应该为nSamples * nDim。

<Sampling Function Definitions> +=
    COREDLL void LatinHypercube(float *samples, int nSamples, int nDim) {
        <Generate LHS samples along diagonal>
        <Permute LHS samples in each dimension>
    }

<Generate LHS samples along diagonal> =
    float delta = 1.f / nSamples;
    for (int i = 0; i < nSamples; ++i)
        for(int j = 0; j < nDim; ++j)
            samples[nDim *i + j] = (i + RandomFloat()) * delta;

在进行排列时,该函数对所有采样进行循环,一次在一个维上做随机排列。注意这跟前面所提到的Shuffle()函数是不同的排列:Shuffle()把移动整个的nDims元素所做构成的块(下图的(a)),而LatinHypercube()对每个维的采样进行独立的排列(下图的(b))。

pbrt-072-07.jpg 

<Premute LHS samples in each dimension> =
    for (int i = 0; i < nDim; ++i) {
        for (int j = 0; j < nSamples; ++j) {
            u_int other = RandomUInt % nSamples;
            swap(samples[nDim * j + i], sample[nDim * other + i]);
        }
    }

对于图7.22中的场景:

pbrt-072-08.jpg 
        (图7.22)

图7.23显示了为DirectLighting积分器所做的采样改进。在图(a)中,每个像素用了1个图像采样,每个图像采样用了16个阴影采样;在图(a)中,每个像素用了16个图像采样,每个图像采样用了1个阴影采样。由于StratifiedSampler可以生成良好的LHS模式,所以第一个例子有更好的阴影质量,尽管两者所用的阴影采样数目相同。

pbrt-072-09.jpg

原创粉丝点击