Physically Based Rendering From Theory To Implementation
该系列属于系统源代码解读系列,根据源码程序,分析程序处理路径,从系统层面探索基于物理的渲染的奥秘。
该系列为译者原创,转载请说明原文出处。
作者:Elsa的迷弟
个人blog网址:https://blog.csdn.net/weixin_44518102
3. 开始渲染
在上一节中,我们使用BasicSceneBuilder
解析文件,将处理数据保存到BasicScene scene;
中。
RenderCPU(scene);
之后调用RenderCPU进入渲染阶段
void RenderCPU(BasicScene &parsedScene) {
Allocator alloc;
ThreadLocal<Allocator> threadAllocators([]() { return Allocator(); });
3.1 初始化阶段
首先,创建介质,因为该场景介质为空,因此跳过。
// Create media first (so have them for the camera...)
std::map<std::string, Medium> media = parsedScene.CreateMedia();
之后创建纹理,纹理由Token == "Texture"
指定,这里同样跳过。
NamedTextures textures = parsedScene.CreateTextures();
然后创建场景光源Lights,之后是材质、加速结构
// Lights
std::map<int, pstd::vector<Light> *> shapeIndexToAreaLights;
std::vector<Light> lights =
parsedScene.CreateLights(textures, &shapeIndexToAreaLights);
// Materials
std::map<std::string, pbrt::Material> namedMaterials;
std::vector<pbrt::Material> materials;
parsedScene.CreateMaterials(textures, &namedMaterials, &materials);
// Primitive
Primitive accel = parsedScene.CreateAggregate(textures, shapeIndexToAreaLights, media,
namedMaterials, materials);
跳过一些Helpful warnings,直接进入渲染历程
// Render!
integrator->Render();
积分器有多种子类,我们进入默认的积分器渲染ImageTileIntegrator::Render();
首先我们在每个线程中创建局部变量threadPixel,threadSampleIndex;以及每个线程的ScratchBuffer,Sampler。
void ImageTileIntegrator::Render() {
thread_local Point2i threadPixel;
thread_local int threadSampleIndex;
// 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);
然后,开始渲染,渲染分为步进渲染和一次渲染,步进渲染每次只渲染少数采样数并显示,之后在逐步渲染更多采样数,逐步增强渲染效果。一次渲染则是一次性渲染所有spp,最后再显示。
int waveStart = 0, waveEnd = 1, nextWaveSize = 1;
渲染!!!
有关ParallelFor2D的相关内容可参照 附录 B.6 并行 章节 B.6.5 Parallel for Loops 循环并行化
// Render image in waves
while (waveStart < spp) {
// Render current wave's image tiles in parallel
// 并行渲染当前循环图像块
ParallelFor2D(pixelBounds, [&](Bounds2i tileBounds) {
// Render image tile given by _tileBounds_
// 渲染由tileBounds给出的块大小
ScratchBuffer &scratchBuffer = scratchBuffers.Get();
Sampler &sampler = samplers.Get();
PBRT_DBG("Starting image tile (%d,%d)-(%d,%d) waveStart %d, waveEnd %d\n",
tileBounds.pMin.x, tileBounds.pMin.y, tileBounds.pMax.x,
tileBounds.pMax.y, waveStart, waveEnd);
for (Point2i pPixel : tileBounds) {
StatsReportPixelStart(pPixel);
threadPixel = pPixel;
// Render samples in pixel _pPixel_
// 以像素pPixel渲染样本
for (int sampleIndex = waveStart; sampleIndex < waveEnd; ++sampleIndex) {
threadSampleIndex = sampleIndex;
// 设置采样器的数据值
sampler.StartPixelSample(pPixel, sampleIndex);
// 负责确定指定样本的值
EvaluatePixelSample(pPixel, sampleIndex, sampler, scratchBuffer);
// 释放它在ScratchBuffer中分配的临时内存。
scratchBuffer.Reset();
}
StatsReportPixelEnd(pPixel);
}
PBRT_DBG("Finished image tile (%d,%d)-(%d,%d)\n", tileBounds.pMin.x,
tileBounds.pMin.y, tileBounds.pMax.x, tileBounds.pMax.y);
// 更新进度
progress.Update((waveEnd - waveStart) * tileBounds.Area());
});
多个线程并行处理lambda函数,核心集中在如下代码中(删除了调试代码):
for (Point2i pPixel : tileBounds) {
// Render samples in pixel _pPixel_
// 以像素pPixel渲染样本
for (int sampleIndex = waveStart; sampleIndex < waveEnd; ++sampleIndex) {
// 提供图像中像素的坐标和像素内样本的索引,确定每次采样点位置。
sampler.StartPixelSample(pPixel, sampleIndex);
// 负责确定指定样本的值
EvaluatePixelSample(pPixel, sampleIndex, sampler, scratchBuffer);
// 释放它在ScratchBuffer中分配的临时内存。
scratchBuffer.Reset();
}
StatsReportPixelEnd(pPixel);
}
StartPixelSample函数调用了HaltonSampler::StartPixelSample()函数。
EvaluatePixelSample(pPixel, sampleIndex, sampler, scratchBuffer);
EvaluatePixelSample根据当前采样点生成采样数据。整个流程如下注释:
void RayIntegrator::EvaluatePixelSample(Point2i pPixel, int sampleIndex, Sampler sampler,
ScratchBuffer &scratchBuffer) {
// 1. 随机获取射线的样本波长
// 2. 初始化CameraSample
// 3. 对当前相机采样点生成渲染空间下的光线
// 4. 如果有效,则追踪cameraRay
// if(有效){
// 4.1 沿光线方向获取辐射度
L = cameraRay->weight * Li(cameraRay->ray, lambda, sampler, scratchBuffer,
initializeVisibleSurface ? &visibleSurface : nullptr);
// }
// 5. 增加相机光线对图像的贡献
其中重点关注L = cameraRay->weight * Li(cameraRay->ray, lambda, sampler, scratchBuffer,initializeVisibleSurface ? &visibleSurface : nullptr);
代码,该代码为光线追踪的核心调用。
此处场景使用了RandomWalkIntegrator::Li()作为实际调用
SampledSpectrum Li(RayDifferential ray, SampledWavelengths &lambda, Sampler sampler,
ScratchBuffer &scratchBuffer,
VisibleSurface *visibleSurface) const {
return LiRandomWalk(ray, lambda, sampler, scratchBuffer, 0);
}
Li函数直接调用LiRandomWalk(ray, lambda, sampler, scratchBuffer, 0);
该函数首先在场景中发射光线,判断是否有物体与之相交,如果有,则获取相交信息。
该相交信息通过采样器相交函数调用加速器相交函数,加速器相交函数(一般为BVHAggregate::Intersect),之后定位到到shape的相交函数。具体的返回信息由每个类型的相交函数确定。
SampledSpectrum LiRandomWalk(RayDifferential ray, SampledWavelengths &lambda,
Sampler sampler, ScratchBuffer &scratchBuffer,
int depth) const {
// Intersect ray with scene and return if no intersection
// 将光线与场景相交,如果没有交集则返回
pstd::optional<ShapeIntersection> si = Intersect(ray);
if (!si) {
// Return emitted light from infinite light sources
// 如果没有交集,则从环境光中获取入射光,并直接返回
SampledSpectrum Le(0.f);
for (Light light : infiniteLights)
Le += light.Le(ray, lambda);
return Le;
}
如果有相交信息,首先获取表面交叉点的自发光辐射,保存在Le中。
SurfaceInteraction &isect = si->intr;
// Get emitted radiance at surface intersection
// 得到表面交叉点的自发光辐射
Vector3f wo = -ray.d;
SampledSpectrum Le = isect.Le(wo, lambda);
接下来计算由该相交点反射的辐射,首先随机在球面表面生成入射光线方向,之后计算该分析的BSDF值
// Randomly sample direction leaving surface for random walk
// 计算随机游走交叉点的BSDF
// 这里获取了随机球表面的(x,y,z)坐标
Point2f u = sampler.Get2D();
Vector3f wp = SampleUniformSphere(u);
// Evaluate BSDF at surface for sampled direction
// 评估采样方向表面的BSDF
SampledSpectrum fcos = bsdf.f(wo, wp) * AbsDot(wp, isect.shading.n);
if (!fcos)
return Le;
同样,我们也需要该方向上的入射辐射值。首先生成该方向光线,之后递归调用LiRandomWalk函数。
// Recursively trace ray to estimate incident radiance at surface
// 递归跟踪光线估计表面入射辐射度
ray = isect.SpawnRay(wp);
return Le + fcos * LiRandomWalk(ray, lambda, sampler, scratchBuffer, depth + 1) /
(1 / (4 * Pi));
直到达到终止条件。
// Terminate random walk if maximum depth has been reached
// 如果到达最大深度,则终止随机游走
if (depth == maxDepth)
return Le;
返回的值被加权保存在变量L中,最终将这个变量传给Film,生成图像。