【代码解读】3. 开始渲染 - Physically Based Rendering From Theory To Implementation(PBRT)

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,生成图像。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elsa的迷弟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值