Q135:PBRT-V3,随机渐进光子映射(Stochastic Progressive Photon Mapping)(16.2章节)

198 篇文章 12 订阅
195 篇文章 27 订阅

一、概述

一开始出现的是Photon Mapping,后来有了Progressive Photon Mapping,后来才有了Stochastic Progressive Photon Mapping。
关于这三个的前世今生等相互关联,参考:
photon mapping学习笔记
再谈光子映射

前面的都不是重点。
咱关注的是PBRT-V3上介绍的SPPM(Stochastic Progressive Photon Mapping)的原理及其C++代码实现。

二、SPPM的原理

SPPM是PPM的改版(或者说“升级版”)。SPPM的优势是没有内存的限制。

SPPM算法有如下几个步骤:

1,由相机产生visible points(“visible point”,即“(对相机来说)可见的点”。基本是一个像素点对应一个visible point,所以很省内存。这个过程中会计算visible point的“直接光照”)。
2,将所有visible points保存到一个grid中。
3,由光源发出光子光线给grid中的visible points“送光”。
4,步骤1、2、3结束,意味着一次迭代将要结束,将要开始下一次迭代。在开始下一次迭代之前,根据当前迭代的情况调整下一次迭代光子“送光”是的搜索半径。然后,回到步骤1开始下一次迭代。

5、所有迭代完成后,计算每一个visible point最终的“间接光照”,在“直接光照”的基础上添加“间接光照”,输出图形。

2.1 产生visible points

前面已经提到“visible points”是指“对相机来说,可见的点”。
那么,场景中那些点对相机来说是可见的呢?
从相机产生一条光线进入场景,“visible point”指的是:
1,光线直接diffuse surface相交的交点;
2,光线发生specular (reflection/transmission) 之后,和diffuse surface相交的交点。

这里写图片描述

一般情况下(即,不考虑光线在specular surfaces之间弹来弹去和打出场景),一个像素点,对应一条光线,对应一个visible point。

保存visible point的数据:

这里写图片描述

其中,特别说明一下Ld、radius。

Ld,visible point处的直接光照;

radius,visible point的“搜索半径”/“收光半径”。对于每一个visible point,当前只保存了“直接光照”。后续,来自光源的光子光线给每个visible point送来的光是属于“间接光照”。光子光线“送光”的方式:光线和场景中某surface相交产生一个交点,该交点附近的所有visible points都有可能收到当前光子光线送来的光,只要visible point到该交点的距离小于或者等于该visible point的“搜索半径”/“收光半径”。

2.2 构建grid

grid的每个cell中保存着一个visible point链表。

前面提到,每一个visible point都有自己的“搜索半径”/“收光半径”。
这个就相当于一个“以visible point为球心,以“收光半径”为半径“的球。
visible point对应的这个球可能和grid中的多个cell发生重叠,所以需要将这个visible point添加到有重叠的多个cell的“visible point链表”中。

另外,可能存在多个visible point对应的球面和同一个cell发生重叠,所以,一个cell的“visible point链表”中会保存所有“对应球面和该cell发生重叠”的visible point。

这里写图片描述

2.3 光子光线“送光”

来自光源的光子光线和场景中的某surface相交,得到一个交点。
将该交点坐标转换到“前面根据visible point构建”的grid中。
该交点经过转换后会对应grid中的某个cell。
该cell中保存了一个visible point的链表。
这条光子光线将给这个cell对应的visible point链表中的所有visible points“送光”。光子光线给visible point送来的光即为visible point处的“间接光照”。

光子光线“送光”是这样的:送完一家,送另一家。

这个有点类似于path tracer中path的构建。

这里写图片描述

关于“送光”的量,这里也是用beta表示(这个beta和visible point中保存的那个beta的物理意义是不一样的。两个beta在程序中的作用范围不一样,所以不会冲突)。
初始beta(即光子光线对应path的长度为1时):
这里写图片描述
(这里面涉及到光子光线的采样,参考16.1.2章节,此处不表)

随着光子光线对应path的长度的增加,beta值发生改变。
这里涉及到一个问题:光子光线不能是无止尽地在场景中延伸,那么怎么终止光子光线呢?
一个光子撞击物体后,是继续传播呢,还是被物体吸收呢?这个用Russian roulette来决定。
β的计算如下:
这里写图片描述

2.4 调整visible point的“搜索半径”

对于每一迭代(对应若干条光子光线),相关数据的计算如下放截图:

这里写图片描述

当最后一次迭代结束时,计算光源给某个visible point送光所产生的效果(visible point的间接光照):

这里写图片描述

正因为“搜索半径”的存在,所以SPPM是一个biased的算法。
当然,实际使用中,随着迭代次数的增加,“搜索半径”越来越小,从而biased的程度越来越小,得到的图形也越来越精确。

2.5 关于单次迭代的光子数和迭代次数

直接看张图吧:

这里写图片描述

如a图,单次迭代的光子数为10000个,迭代1000次。
(关于单次迭代的光子数,PBRT-V3官方代码中做法:若用户有设置单次迭代的光子数,就用这个数值;若用户没有设置单次迭代的光子数,就把像素点的个数作为单次迭代的光子数的默认值。
关于迭代次数,由于不涉及内存问题,可由用户随意设置)

三、SPPM的C++代码实现

PBRT-V3中对应的积分器是SPPMIntegrator。
SPPMIntegrator不是SamplerIntegrator,所以要实现自己的render()成员方法。

先截取需要特别说明的代码段,在贴出完整的render()成员方法。

产生visible point时:

这里写图片描述

这里写图片描述

给visible point送光时:

这里写图片描述

这里写图片描述

这里写图片描述

调整visible point的“搜索半径”时:

这里写图片描述

给visible point添加最终的间接光照时:

这里写图片描述

完整render()代码如下:

// SPPM Method Definitions
void SPPMIntegrator::Render(const Scene &scene) {
    ProfilePhase p(Prof::IntegratorRender);
    // Initialize _pixelBounds_ and _pixels_ array for SPPM
    Bounds2i pixelBounds = camera->film->croppedPixelBounds;
    int nPixels = pixelBounds.Area();
    std::unique_ptr<SPPMPixel[]> pixels(new SPPMPixel[nPixels]);
    for (int i = 0; i < nPixels; ++i) pixels[i].radius = initialSearchRadius;
    const Float invSqrtSPP = 1.f / std::sqrt(nIterations);
    pixelMemoryBytes = nPixels * sizeof(SPPMPixel);
    // Compute _lightDistr_ for sampling lights proportional to power
    std::unique_ptr<Distribution1D> lightDistr =
        ComputeLightPowerDistribution(scene);

    // Perform _nIterations_ of SPPM integration
    HaltonSampler sampler(nIterations, pixelBounds);

    // Compute number of tiles to use for SPPM camera pass
    Vector2i pixelExtent = pixelBounds.Diagonal();
    const int tileSize = 16;
    Point2i nTiles((pixelExtent.x + tileSize - 1) / tileSize,
                   (pixelExtent.y + tileSize - 1) / tileSize);
    ProgressReporter progress(2 * nIterations, "Rendering");
    for (int iter = 0; iter < nIterations; ++iter) {
        // Generate SPPM visible points
        std::vector<MemoryArena> perThreadArenas(MaxThreadIndex());
        {
            ProfilePhase _(Prof::SPPMCameraPass);
            ParallelFor2D([&](Point2i tile) {
                MemoryArena &arena = perThreadArenas[ThreadIndex];
                // Follow camera paths for _tile_ in image for SPPM
                int tileIndex = tile.y * nTiles.x + tile.x;
                std::unique_ptr<Sampler> tileSampler = sampler.Clone(tileIndex);

                // Compute _tileBounds_ for SPPM tile
                int x0 = pixelBounds.pMin.x + tile.x * tileSize;
                int x1 = std::min(x0 + tileSize, pixelBounds.pMax.x);
                int y0 = pixelBounds.pMin.y + tile.y * tileSize;
                int y1 = std::min(y0 + tileSize, pixelBounds.pMax.y);
                Bounds2i tileBounds(Point2i(x0, y0), Point2i(x1, y1));
                for (Point2i pPixel : tileBounds) {
                    // Prepare _tileSampler_ for _pPixel_
                    tileSampler->StartPixel(pPixel);
                    tileSampler->SetSampleNumber(iter);

                    // Generate camera ray for pixel for SPPM
                    CameraSample cameraSample =
                        tileSampler->GetCameraSample(pPixel);
                    RayDifferential ray;
                    Spectrum beta =
                        camera->GenerateRayDifferential(cameraSample, &ray);
                    ray.ScaleDifferentials(invSqrtSPP);

                    // Follow camera ray path until a visible point is created

                    // Get _SPPMPixel_ for _pPixel_
                    Point2i pPixelO = Point2i(pPixel - pixelBounds.pMin);
                    int pixelOffset =
                        pPixelO.x +
                        pPixelO.y * (pixelBounds.pMax.x - pixelBounds.pMin.x);
                    SPPMPixel &pixel = pixels[pixelOffset];
                    bool specularBounce = false;
                    for (int depth = 0; depth < maxDepth; ++depth) {
                        SurfaceInteraction isect;
                        ++totalPhotonSurfaceInteractions;
                        if (!scene.Intersect(ray, &isect)) {
                            // Accumulate light contributions for ray with no
                            // intersection
                            for (const auto &light : scene.lights)
                                pixel.Ld += beta * light->Le(ray);
                            break;
                        }
                        // Process SPPM camera ray intersection

                        // Compute BSDF at SPPM camera ray intersection
                        isect.ComputeScatteringFunctions(ray, arena, true);
                        if (!isect.bsdf) {
                            ray = isect.SpawnRay(ray.d);
                            --depth;
                            continue;
                        }
                        const BSDF &bsdf = *isect.bsdf;

                        // Accumulate direct illumination at SPPM camera ray
                        // intersection
                        Vector3f wo = -ray.d;
                        if (depth == 0 || specularBounce)
                            pixel.Ld += beta * isect.Le(wo);
                        pixel.Ld +=
                            beta * UniformSampleOneLight(isect, scene, arena,
                                                         *tileSampler);

                        // Possibly create visible point and end camera path
                        bool isDiffuse = bsdf.NumComponents(BxDFType(
                                             BSDF_DIFFUSE | BSDF_REFLECTION |
                                             BSDF_TRANSMISSION)) > 0;
                        bool isGlossy = bsdf.NumComponents(BxDFType(
                                            BSDF_GLOSSY | BSDF_REFLECTION |
                                            BSDF_TRANSMISSION)) > 0;
                        if (isDiffuse || (isGlossy && depth == maxDepth - 1)) {
                            pixel.vp = {isect.p, wo, &bsdf, beta};
                            break;
                        }

                        // Spawn ray from SPPM camera path vertex
                        if (depth < maxDepth - 1) {
                            Float pdf;
                            Vector3f wi;
                            BxDFType type;
                            Spectrum f =
                                bsdf.Sample_f(wo, &wi, tileSampler->Get2D(),
                                              &pdf, BSDF_ALL, &type);
                            if (pdf == 0. || f.IsBlack()) break;
                            specularBounce = (type & BSDF_SPECULAR) != 0;
                            beta *= f * AbsDot(wi, isect.shading.n) / pdf;
                            if (beta.y() < 0.25) {
                                Float continueProb =
                                    std::min((Float)1, beta.y());
                                if (tileSampler->Get1D() > continueProb) break;
                                beta /= continueProb;
                            }
                            ray = (RayDifferential)isect.SpawnRay(wi);
                        }
                    }
                }
            }, nTiles);
        }
        progress.Update();

        // Create grid of all SPPM visible points
        int gridRes[3];
        Bounds3f gridBounds;
        // Allocate grid for SPPM visible points
        const int hashSize = nPixels;
        std::vector<std::atomic<SPPMPixelListNode *>> grid(hashSize);
        {
            ProfilePhase _(Prof::SPPMGridConstruction);

            // Compute grid bounds for SPPM visible points
            Float maxRadius = 0.;
            for (int i = 0; i < nPixels; ++i) {
                const SPPMPixel &pixel = pixels[i];
                if (pixel.vp.beta.IsBlack()) continue;
                Bounds3f vpBound = Expand(Bounds3f(pixel.vp.p), pixel.radius);
                gridBounds = Union(gridBounds, vpBound);
                maxRadius = std::max(maxRadius, pixel.radius);
            }

            // Compute resolution of SPPM grid in each dimension
            Vector3f diag = gridBounds.Diagonal();
            Float maxDiag = MaxComponent(diag);
            int baseGridRes = (int)(maxDiag / maxRadius);
            CHECK_GT(baseGridRes, 0);
            for (int i = 0; i < 3; ++i)
                gridRes[i] = std::max((int)(baseGridRes * diag[i] / maxDiag), 1);

            // Add visible points to SPPM grid
            ParallelFor([&](int pixelIndex) {
                MemoryArena &arena = perThreadArenas[ThreadIndex];
                SPPMPixel &pixel = pixels[pixelIndex];
                if (!pixel.vp.beta.IsBlack()) {
                    // Add pixel's visible point to applicable grid cells
                    Float radius = pixel.radius;
                    Point3i pMin, pMax;
                    ToGrid(pixel.vp.p - Vector3f(radius, radius, radius),
                           gridBounds, gridRes, &pMin);
                    ToGrid(pixel.vp.p + Vector3f(radius, radius, radius),
                           gridBounds, gridRes, &pMax);
                    for (int z = pMin.z; z <= pMax.z; ++z)
                        for (int y = pMin.y; y <= pMax.y; ++y)
                            for (int x = pMin.x; x <= pMax.x; ++x) {
                                // Add visible point to grid cell $(x, y, z)$
                                int h = hash(Point3i(x, y, z), hashSize);
                                SPPMPixelListNode *node =
                                    arena.Alloc<SPPMPixelListNode>();
                                node->pixel = &pixel;

                                // Atomically add _node_ to the start of
                                // _grid[h]_'s linked list
                                node->next = grid[h];
                                while (grid[h].compare_exchange_weak(
                                           node->next, node) == false)
                                    ;
                            }
                    ReportValue(gridCellsPerVisiblePoint,
                                (1 + pMax.x - pMin.x) * (1 + pMax.y - pMin.y) *
                                    (1 + pMax.z - pMin.z));
                }
            }, nPixels, 4096);
        }

        // Trace photons and accumulate contributions
        {
            ProfilePhase _(Prof::SPPMPhotonPass);
            std::vector<MemoryArena> photonShootArenas(MaxThreadIndex());
            ParallelFor([&](int photonIndex) {
                MemoryArena &arena = photonShootArenas[ThreadIndex];
                // Follow photon path for _photonIndex_
                uint64_t haltonIndex =
                    (uint64_t)iter * (uint64_t)photonsPerIteration +
                    photonIndex;
                int haltonDim = 0;

                // Choose light to shoot photon from
                Float lightPdf;
                Float lightSample = RadicalInverse(haltonDim++, haltonIndex);
                int lightNum =
                    lightDistr->SampleDiscrete(lightSample, &lightPdf);
                const std::shared_ptr<Light> &light = scene.lights[lightNum];

                // Compute sample values for photon ray leaving light source
                Point2f uLight0(RadicalInverse(haltonDim, haltonIndex),
                                RadicalInverse(haltonDim + 1, haltonIndex));
                Point2f uLight1(RadicalInverse(haltonDim + 2, haltonIndex),
                                RadicalInverse(haltonDim + 3, haltonIndex));
                Float uLightTime =
                    Lerp(RadicalInverse(haltonDim + 4, haltonIndex),
                         camera->shutterOpen, camera->shutterClose);
                haltonDim += 5;

                // Generate _photonRay_ from light source and initialize _beta_
                RayDifferential photonRay;
                Normal3f nLight;
                Float pdfPos, pdfDir;
                Spectrum Le =
                    light->Sample_Le(uLight0, uLight1, uLightTime, &photonRay,
                                     &nLight, &pdfPos, &pdfDir);
                if (pdfPos == 0 || pdfDir == 0 || Le.IsBlack()) return;
                Spectrum beta = (AbsDot(nLight, photonRay.d) * Le) /
                                (lightPdf * pdfPos * pdfDir);
                if (beta.IsBlack()) return;

                // Follow photon path through scene and record intersections
                SurfaceInteraction isect;
                for (int depth = 0; depth < maxDepth; ++depth) {
                    if (!scene.Intersect(photonRay, &isect)) break;
                    ++totalPhotonSurfaceInteractions;
                    if (depth > 0) {
                        // Add photon contribution to nearby visible points
                        Point3i photonGridIndex;
                        if (ToGrid(isect.p, gridBounds, gridRes,
                                   &photonGridIndex)) {
                            int h = hash(photonGridIndex, hashSize);
                            // Add photon contribution to visible points in
                            // _grid[h]_
                            for (SPPMPixelListNode *node =
                                     grid[h].load(std::memory_order_relaxed);
                                 node != nullptr; node = node->next) {
                                ++visiblePointsChecked;
                                SPPMPixel &pixel = *node->pixel;
                                Float radius = pixel.radius;
                                if (DistanceSquared(pixel.vp.p, isect.p) >
                                    radius * radius)
                                    continue;
                                // Update _pixel_ $\Phi$ and $M$ for nearby
                                // photon
                                Vector3f wi = -photonRay.d;
                                Spectrum Phi =
                                    beta * pixel.vp.bsdf->f(pixel.vp.wo, wi);
                                for (int i = 0; i < Spectrum::nSamples; ++i)
                                    pixel.Phi[i].Add(Phi[i]);
                                ++pixel.M;
                            }
                        }
                    }
                    // Sample new photon ray direction

                    // Compute BSDF at photon intersection point
                    isect.ComputeScatteringFunctions(photonRay, arena, true,
                                                     TransportMode::Importance);
                    if (!isect.bsdf) {
                        --depth;
                        photonRay = isect.SpawnRay(photonRay.d);
                        continue;
                    }
                    const BSDF &photonBSDF = *isect.bsdf;

                    // Sample BSDF _fr_ and direction _wi_ for reflected photon
                    Vector3f wi, wo = -photonRay.d;
                    Float pdf;
                    BxDFType flags;

                    // Generate _bsdfSample_ for outgoing photon sample
                    Point2f bsdfSample(
                        RadicalInverse(haltonDim, haltonIndex),
                        RadicalInverse(haltonDim + 1, haltonIndex));
                    haltonDim += 2;
                    Spectrum fr = photonBSDF.Sample_f(wo, &wi, bsdfSample, &pdf,
                                                      BSDF_ALL, &flags);
                    if (fr.IsBlack() || pdf == 0.f) break;
                    Spectrum bnew =
                        beta * fr * AbsDot(wi, isect.shading.n) / pdf;

                    // Possibly terminate photon path with Russian roulette
                    Float q = std::max((Float)0, 1 - bnew.y() / beta.y());
                    if (RadicalInverse(haltonDim++, haltonIndex) < q) break;
                    beta = bnew / (1 - q);
                    photonRay = (RayDifferential)isect.SpawnRay(wi);
                }
                arena.Reset();
            }, photonsPerIteration, 8192);
            progress.Update();
            photonPaths += photonsPerIteration;
        }

        // Update pixel values from this pass's photons
        {
            ProfilePhase _(Prof::SPPMStatsUpdate);
            ParallelFor([&](int i) {
                SPPMPixel &p = pixels[i];
                if (p.M > 0) {
                    // Update pixel photon count, search radius, and $\tau$ from
                    // photons
                    Float gamma = (Float)2 / (Float)3;
                    Float Nnew = p.N + gamma * p.M;
                    Float Rnew = p.radius * std::sqrt(Nnew / (p.N + p.M));
                    Spectrum Phi;
                    for (int j = 0; j < Spectrum::nSamples; ++j)
                        Phi[j] = p.Phi[j];
                    p.tau = (p.tau + p.vp.beta * Phi) * (Rnew * Rnew) /
                            (p.radius * p.radius);
                    p.N = Nnew;
                    p.radius = Rnew;
                    p.M = 0;
                    for (int j = 0; j < Spectrum::nSamples; ++j)
                        p.Phi[j] = (Float)0;
                }
                // Reset _VisiblePoint_ in pixel
                p.vp.beta = 0.;
                p.vp.bsdf = nullptr;
            }, nPixels, 4096);
        }

        // Periodically store SPPM image in film and write image
        if (iter + 1 == nIterations || ((iter + 1) % writeFrequency) == 0) {
            int x0 = pixelBounds.pMin.x;
            int x1 = pixelBounds.pMax.x;
            uint64_t Np = (uint64_t)(iter + 1) * (uint64_t)photonsPerIteration;
            std::unique_ptr<Spectrum[]> image(new Spectrum[pixelBounds.Area()]);
            int offset = 0;
            for (int y = pixelBounds.pMin.y; y < pixelBounds.pMax.y; ++y) {
                for (int x = x0; x < x1; ++x) {
                    // Compute radiance _L_ for SPPM pixel _pixel_
                    const SPPMPixel &pixel =
                        pixels[(y - pixelBounds.pMin.y) * (x1 - x0) + (x - x0)];
                    Spectrum L = pixel.Ld / (iter + 1);
                    L += pixel.tau / (Np * Pi * pixel.radius * pixel.radius);
                    image[offset++] = L;
                }
            }
            camera->film->SetImage(image.get());
            camera->film->WriteImage();
            // Write SPPM radius image, if requested
            if (getenv("SPPM_RADIUS")) {
                std::unique_ptr<Float[]> rimg(
                    new Float[3 * pixelBounds.Area()]);
                Float minrad = 1e30f, maxrad = 0;
                for (int y = pixelBounds.pMin.y; y < pixelBounds.pMax.y; ++y) {
                    for (int x = x0; x < x1; ++x) {
                        const SPPMPixel &p =
                            pixels[(y - pixelBounds.pMin.y) * (x1 - x0) +
                                   (x - x0)];
                        minrad = std::min(minrad, p.radius);
                        maxrad = std::max(maxrad, p.radius);
                    }
                }
                fprintf(stderr,
                        "iterations: %d (%.2f s) radius range: %f - %f\n",
                        iter + 1, progress.ElapsedMS() / 1000., minrad, maxrad);
                int offset = 0;
                for (int y = pixelBounds.pMin.y; y < pixelBounds.pMax.y; ++y) {
                    for (int x = x0; x < x1; ++x) {
                        const SPPMPixel &p =
                            pixels[(y - pixelBounds.pMin.y) * (x1 - x0) +
                                   (x - x0)];
                        Float v = 1.f - (p.radius - minrad) / (maxrad - minrad);
                        rimg[offset++] = v;
                        rimg[offset++] = v;
                        rimg[offset++] = v;
                    }
                }
                Point2i res(pixelBounds.pMax.x - pixelBounds.pMin.x,
                            pixelBounds.pMax.y - pixelBounds.pMin.y);
                WriteImage("sppm_radius.png", rimg.get(), pixelBounds, res);
            }
        }
    }
    progress.Done();
}

四、个人笔记

如下是原始笔记的截图。记录的是最初的理解(有的错误修正了,有的没有修正)和过程中的“吐槽”。

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值