Q135:PBRT-V3,随机渐进光子映射(Stochastic Progressive Photon Mapping)(16.2章节)
来源:互联网 发布:炮打白宫知乎 编辑:程序博客网 时间:2024/05/17 09:37
一、概述
一开始出现的是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 Definitionsvoid 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();}
四、个人笔记
如下是原始笔记的截图。记录的是最初的理解(有的错误修正了,有的没有修正)和过程中的“吐槽”。
- Q135:PBRT-V3,随机渐进光子映射(Stochastic Progressive Photon Mapping)(16.2章节)
- Q119:PBRT-V3,“复合重要性采样”(13.10章节)
- Q120:PBRT-V3,“直接光照”积分器(14.3章节)
- Q124:PBRT-V3,“路径追踪”积分器(14.5章节)
- Q133:PBRT-V3,BSSRDF的采样(15.4章节)
- Q139:PBRT-V3,Metropolis Light Transport (MLT)(16.4章节)
- Q130:PBRT-V3,非均匀介质的采样(11.3.3章节、15.2.2章节)
- Q120:PBRT-V3,“直接光照”积分器(14.3章节)(翻译不下去了)
- Q121:PBRT-V3,光传播方程(The Light Transport Equation)(14.4章节)
- Q134:PBRT-V3,次表面散射(Subsurface Scattering)(15.5章节)
- Q136:PBRT-V3,双向路径追踪(Bidirectional Path Tracing)(16.3章节)
- Q138:PBRT-V3,伪随机数发生器(pseudo-random number generator,RNG)(A.1.2章节)
- Q128:PBRT-V3,“体渲染”积分器的“传播方程”(15.1章节)
- Q129:PBRT-V3,均匀介质的采样(15.2.1章节)
- Q141:PBRT-V3,交点处各种微分的求解(球面,3.2章节)
- Q142:PBRT-V3,交点处各种微分的求解(三角形,3.6章节)
- Q132:PBRT-V3,BSSRDF(双向散射表面反射分布函数)(5.6.2章节、11.4章节)
- 随机规划(Stochastic Programming)
- String,StringBuffer和StringBuilder的区别
- 喷水装置(一)
- [BZOJ2342] SHOI2011 双倍回文 manacher O(n)
- linux增加swap
- 缺少spring.jar包的低级错误
- Q135:PBRT-V3,随机渐进光子映射(Stochastic Progressive Photon Mapping)(16.2章节)
- eclipse安装Fat jar打包工具
- HDU 6053 TrickGCD(莫比乌斯反演+分块)
- Linux驱动笔记:MTD子系统
- 7.27--SSH学习之SpringMVC,Ajax请求、拦截器、文件上传和MyBatis访问数据库基本操作
- hdu--6045 Is Derek Lying
- 快速删除电脑上的大文件
- 南阳acm士兵杀敌系列
- 问题:C-Kermit>c Sorry, you must SET LINE or SET HOST first