Пример #1
0
void WriteImage(const std::string &name, const Float *rgb,
                const Bounds2i &outputBounds, const Point2i &totalResolution,
                Float gamma) {
    if (name.size() >= 5) {
        size_t suffixOffset = name.size() - 4;
        if (!strcmp(name.c_str() + suffixOffset, ".exr") ||
            !strcmp(name.c_str() + suffixOffset, ".EXR")) {
            WriteImageEXR(name, rgb, outputBounds.pMax.x - outputBounds.pMin.x,
                          outputBounds.pMax.y - outputBounds.pMin.y,
                          totalResolution.x, totalResolution.y,
                          outputBounds.pMin.x, outputBounds.pMin.y);
            return;
        }
        if (!strcmp(name.c_str() + suffixOffset, ".pfm") ||
            !strcmp(name.c_str() + suffixOffset, ".PFM")) {
            Vector2i resolution = outputBounds.Diagonal();
            WriteImagePFM(name, rgb, resolution.x, resolution.y);
            return;
        }
        if (!strcmp(name.c_str() + suffixOffset, ".tga") ||
            !strcmp(name.c_str() + suffixOffset, ".TGA") ||
            !strcmp(name.c_str() + suffixOffset, ".png") ||
            !strcmp(name.c_str() + suffixOffset, ".PNG")) {
            // 8-bit formats; apply gamma
            Vector2i resolution = outputBounds.Diagonal();
            std::unique_ptr<uint8_t[]> rgb8(
                new uint8_t[3 * resolution.x * resolution.y]);
            uint8_t *dst = rgb8.get();
            for (int y = 0; y < resolution.y; ++y) {
                for (int x = 0; x < resolution.x; ++x) {
#define TO_BYTE(v) (uint8_t(Clamp(255.f * powf((v), 1.f / gamma), 0.f, 255.f)))
                    dst[0] = TO_BYTE(rgb[3 * (y * resolution.x + x) + 0]);
                    dst[1] = TO_BYTE(rgb[3 * (y * resolution.x + x) + 1]);
                    dst[2] = TO_BYTE(rgb[3 * (y * resolution.x + x) + 2]);
#undef TO_BYTE
                    dst += 3;
                }
            }

            if (!strcmp(name.c_str() + suffixOffset, ".tga") ||
                !strcmp(name.c_str() + suffixOffset, ".TGA"))
                WriteImageTGA(name, rgb8.get(),
                              outputBounds.pMax.x - outputBounds.pMin.x,
                              outputBounds.pMax.y - outputBounds.pMin.y,
                              totalResolution.x, totalResolution.y,
                              outputBounds.pMin.x, outputBounds.pMin.y);
            else if (stbi_write_png(name.c_str(), resolution.x, resolution.y, 3,
                                    rgb8.get(), 3 * resolution.x) == 0)
                Error("Error writing PNG \"%s\"", name.c_str());
            return;
        }
    }
    Error("Can't determine image file type from suffix of filename \"%s\"",
          name.c_str());
}
Пример #2
0
void WriteImage(const std::string &name, const Float *rgb,
                const Bounds2i &outputBounds, const Point2i &totalResolution) {
    Vector2i resolution = outputBounds.Diagonal();
    if (HasExtension(name, ".exr")) {
        WriteImageEXR(name, rgb, resolution.x, resolution.y, totalResolution.x,
                      totalResolution.y, outputBounds.pMin.x,
                      outputBounds.pMin.y);
    } else if (HasExtension(name, ".pfm")) {
        WriteImagePFM(name, rgb, resolution.x, resolution.y);
    } else if (HasExtension(name, ".tga") || HasExtension(name, ".png")) {
        // 8-bit formats; apply gamma
        Vector2i resolution = outputBounds.Diagonal();
        std::unique_ptr<uint8_t[]> rgb8(
            new uint8_t[3 * resolution.x * resolution.y]);
        uint8_t *dst = rgb8.get();
        for (int y = 0; y < resolution.y; ++y) {
            for (int x = 0; x < resolution.x; ++x) {
#define TO_BYTE(v) (uint8_t) Clamp(255.f * GammaCorrect(v) + 0.5f, 0.f, 255.f)
                dst[0] = TO_BYTE(rgb[3 * (y * resolution.x + x) + 0]);
                dst[1] = TO_BYTE(rgb[3 * (y * resolution.x + x) + 1]);
                dst[2] = TO_BYTE(rgb[3 * (y * resolution.x + x) + 2]);
#undef TO_BYTE
                dst += 3;
            }
        }

        if (HasExtension(name, ".tga"))
            WriteImageTGA(name, rgb8.get(), resolution.x, resolution.y,
                          totalResolution.x, totalResolution.y,
                          outputBounds.pMin.x, outputBounds.pMin.y);
        else {
            unsigned int error = lodepng_encode24_file(
                name.c_str(), rgb8.get(), resolution.x, resolution.y);
            if (error != 0)
                Error("Error writing PNG \"%s\": %s", name.c_str(),
                      lodepng_error_text(error));
        }
    } else {
        Error("Can't determine image file type from suffix of filename \"%s\"",
              name.c_str());
    }
}
Пример #3
0
// MLT Method Definitions
Spectrum MLTIntegrator::L(const Scene &scene, MemoryArena &arena,
                          MLTSampler &sampler, int depth, Point2f *samplePos) {
    sampler.SetStream(3, 0);
    // Determine the number of available strategies and pick a specific one
    int s, t, nStrategies;
    if (depth == 0) {
        nStrategies = 1;
        s = 0;
        t = 2;
    } else {
        nStrategies = depth + 2;
        s = std::min((int)(sampler.Get1D() * nStrategies), nStrategies - 1);
        t = nStrategies - s;
    }

    // Generate a camera subpath with exactly _t_ vertices
    Vertex *cameraSubpath = (Vertex *)arena.Alloc<Vertex>(t);
    Bounds2i sampleBounds = camera->film->GetSampleBounds();
    Vector2i diag = sampleBounds.Diagonal();
    *samplePos = Point2f(sampleBounds.pMin.x + diag.x * sampler.Get1D(),
                         sampleBounds.pMin.y + diag.y * sampler.Get1D());
    if (GenerateCameraSubpath(scene, sampler, arena, t, *camera, *samplePos,
                              cameraSubpath) != t)
        return Spectrum(0.f);

    // Generate a light subpath with exactly _s_ vertices
    sampler.SetStream(3, 1);
    Vertex *lightSubpath = (Vertex *)arena.Alloc<Vertex>(s);
    if (GenerateLightSubpath(scene, sampler, arena, s, cameraSubpath[0].time(),
                             *lightDistr, lightSubpath) != s)
        return Spectrum(0.f);

    // Execute connection strategy and return the radiance estimate
    sampler.SetStream(3, 2);
    Spectrum L = ConnectBDPT(scene, lightSubpath, cameraSubpath, s, t,
                             *lightDistr, *camera, sampler, samplePos);
    arena.Reset();
    return L * nStrategies;
}
Пример #4
0
void BDPTIntegrator::Render(const Scene &scene) {
    std::unique_ptr<LightDistribution> lightDistribution =
        CreateLightSampleDistribution(lightSampleStrategy, scene);

    // Compute a reverse mapping from light pointers to offsets into the
    // scene lights vector (and, equivalently, offsets into
    // lightDistr). Added after book text was finalized; this is critical
    // to reasonable performance with 100s+ of light sources.
    std::unordered_map<const Light *, size_t> lightToIndex;
    for (size_t i = 0; i < scene.lights.size(); ++i)
        lightToIndex[scene.lights[i].get()] = i;

    // Partition the image into tiles
    Film *film = camera->film;
    const Bounds2i sampleBounds = film->GetSampleBounds();
    const Vector2i sampleExtent = sampleBounds.Diagonal();
    const int tileSize = 16;
    const int nXTiles = (sampleExtent.x + tileSize - 1) / tileSize;
    const int nYTiles = (sampleExtent.y + tileSize - 1) / tileSize;
    ProgressReporter reporter(nXTiles * nYTiles, "Rendering");

    // Allocate buffers for debug visualization
    const int bufferCount = (1 + maxDepth) * (6 + maxDepth) / 2;
    std::vector<std::unique_ptr<Film>> weightFilms(bufferCount);
    if (visualizeStrategies || visualizeWeights) {
        for (int depth = 0; depth <= maxDepth; ++depth) {
            for (int s = 0; s <= depth + 2; ++s) {
                int t = depth + 2 - s;
                if (t == 0 || (s == 1 && t == 1)) continue;

                std::string filename =
                    StringPrintf("bdpt_d%02i_s%02i_t%02i.exr", depth, s, t);

                weightFilms[BufferIndex(s, t)] = std::unique_ptr<Film>(new Film(
                    film->fullResolution,
                    Bounds2f(Point2f(0, 0), Point2f(1, 1)),
                    std::unique_ptr<Filter>(CreateBoxFilter(ParamSet())),
                    film->diagonal * 1000, filename, 1.f));
            }
        }
    }

    // Render and write the output image to disk
    if (scene.lights.size() > 0) {
        ParallelFor2D([&](const Point2i tile) {
            // Render a single tile using BDPT
            MemoryArena arena;
            int seed = tile.y * nXTiles + tile.x;
            std::unique_ptr<Sampler> tileSampler = sampler->Clone(seed);
            int x0 = sampleBounds.pMin.x + tile.x * tileSize;
            int x1 = std::min(x0 + tileSize, sampleBounds.pMax.x);
            int y0 = sampleBounds.pMin.y + tile.y * tileSize;
            int y1 = std::min(y0 + tileSize, sampleBounds.pMax.y);
            Bounds2i tileBounds(Point2i(x0, y0), Point2i(x1, y1));
            std::unique_ptr<FilmTile> filmTile =
                camera->film->GetFilmTile(tileBounds);
            for (Point2i pPixel : tileBounds) {
                tileSampler->StartPixel(pPixel);
                if (!InsideExclusive(pPixel, pixelBounds))
                    continue;
                do {
                    // Generate a single sample using BDPT
                    Point2f pFilm = (Point2f)pPixel + tileSampler->Get2D();

                    // Trace the camera subpath
                    Vertex *cameraVertices = arena.Alloc<Vertex>(maxDepth + 2);
                    Vertex *lightVertices = arena.Alloc<Vertex>(maxDepth + 1);
                    int nCamera = GenerateCameraSubpath(
                        scene, *tileSampler, arena, maxDepth + 2, *camera,
                        pFilm, cameraVertices);
                    // Get a distribution for sampling the light at the
                    // start of the light subpath. Because the light path
                    // follows multiple bounces, basing the sampling
                    // distribution on any of the vertices of the camera
                    // path is unlikely to be a good strategy. We use the
                    // PowerLightDistribution by default here, which
                    // doesn't use the point passed to it.
                    const Distribution1D *lightDistr =
                        lightDistribution->Lookup(cameraVertices[0].p());
                    // Now trace the light subpath
                    int nLight = GenerateLightSubpath(
                        scene, *tileSampler, arena, maxDepth + 1,
                        cameraVertices[0].time(), *lightDistr, lightToIndex,
                        lightVertices);

                    // Execute all BDPT connection strategies
                    Spectrum L(0.f);
                    for (int t = 1; t <= nCamera; ++t) {
                        for (int s = 0; s <= nLight; ++s) {
                            int depth = t + s - 2;
                            if ((s == 1 && t == 1) || depth < 0 ||
                                depth > maxDepth)
                                continue;
                            // Execute the $(s, t)$ connection strategy and
                            // update _L_
                            Point2f pFilmNew = pFilm;
                            Float misWeight = 0.f;
                            Spectrum Lpath = ConnectBDPT(
                                scene, lightVertices, cameraVertices, s, t,
                                *lightDistr, lightToIndex, *camera, *tileSampler,
                                &pFilmNew, &misWeight);
                            VLOG(2) << "Connect bdpt s: " << s <<", t: " << t <<
                                ", Lpath: " << Lpath << ", misWeight: " << misWeight;
                            if (visualizeStrategies || visualizeWeights) {
                                Spectrum value;
                                if (visualizeStrategies)
                                    value =
                                        misWeight == 0 ? 0 : Lpath / misWeight;
                                if (visualizeWeights) value = Lpath;
                                weightFilms[BufferIndex(s, t)]->AddSplat(
                                    pFilmNew, value);
                            }
                            if (t != 1)
                                L += Lpath;
                            else
                                film->AddSplat(pFilmNew, Lpath);
                        }
                    }
                    VLOG(2) << "Add film sample pFilm: " << pFilm << ", L: " << L <<
                        ", (y: " << L.y() << ")";
                    filmTile->AddSample(pFilm, L);
                    arena.Reset();
                } while (tileSampler->StartNextSample());
            }
            film->MergeFilmTile(std::move(filmTile));
            reporter.Update();
        }, Point2i(nXTiles, nYTiles));
        reporter.Done();
    }
    film->WriteImage(1.0f / sampler->samplesPerPixel);

    // Write buffers for debug visualization
    if (visualizeStrategies || visualizeWeights) {
        const Float invSampleCount = 1.0f / sampler->samplesPerPixel;
        for (size_t i = 0; i < weightFilms.size(); ++i)
            if (weightFilms[i]) weightFilms[i]->WriteImage(invSampleCount);
    }
}
Пример #5
0
// SamplerIntegrator Method Definitions
void SamplerIntegrator::Render(const Scene &scene) {
    ProfilePhase p(Prof::IntegratorRender);
    Preprocess(scene, *sampler);
    // Render image tiles in parallel

    // Compute number of tiles, _nTiles_, to use for parallel rendering
    Bounds2i sampleBounds = camera->film->GetSampleBounds();
    Vector2i sampleExtent = sampleBounds.Diagonal();
    const int tileSize = 16;
    Point2i nTiles((sampleExtent.x + tileSize - 1) / tileSize,
                   (sampleExtent.y + tileSize - 1) / tileSize);
    ProgressReporter reporter(nTiles.x * nTiles.y, "Rendering");
    {
        StatTimer timer(&renderingTime);
        ParallelFor2D([&](Point2i tile) {
            // Render section of image corresponding to _tile_

            // Allocate _MemoryArena_ for tile
            MemoryArena arena;

            // Get sampler instance for tile
            int seed = tile.y * nTiles.x + tile.x;
            std::unique_ptr<Sampler> tileSampler = sampler->Clone(seed);

            // Compute sample bounds for tile
            int x0 = sampleBounds.pMin.x + tile.x * tileSize;
            int x1 = std::min(x0 + tileSize, sampleBounds.pMax.x);
            int y0 = sampleBounds.pMin.y + tile.y * tileSize;
            int y1 = std::min(y0 + tileSize, sampleBounds.pMax.y);
            Bounds2i tileBounds(Point2i(x0, y0), Point2i(x1, y1));

            // Get _FilmTile_ for tile
            std::unique_ptr<FilmTile> filmTile =
                camera->film->GetFilmTile(tileBounds);

            // Loop over pixels in tile to render them
            for (Point2i pixel : tileBounds) {
                {
                    ProfilePhase pp(Prof::StartPixel);
                    tileSampler->StartPixel(pixel);
                }
                do {
                    // Initialize _CameraSample_ for current sample
                    CameraSample cameraSample =
                        tileSampler->GetCameraSample(pixel);

                    // Generate camera ray for current sample
                    RayDifferential ray;
                    Float rayWeight =
                        camera->GenerateRayDifferential(cameraSample, &ray);
                    ray.ScaleDifferentials(
                        1 / std::sqrt((Float)tileSampler->samplesPerPixel));
                    ++nCameraRays;

                    // Evaluate radiance along camera ray
                    Spectrum L(0.f);
                    if (rayWeight > 0) L = Li(ray, scene, *tileSampler, arena);

                    // Issue warning if unexpected radiance value returned
                    if (L.HasNaNs()) {
                        Error(
                            "Not-a-number radiance value returned "
                            "for image sample.  Setting to black.");
                        L = Spectrum(0.f);
                    } else if (L.y() < -1e-5) {
                        Error(
                            "Negative luminance value, %f, returned "
                            "for image sample.  Setting to black.",
                            L.y());
                        L = Spectrum(0.f);
                    } else if (std::isinf(L.y())) {
                        Error(
                            "Infinite luminance value returned "
                            "for image sample.  Setting to black.");
                        L = Spectrum(0.f);
                    }

                    // Add camera ray's contribution to image
                    filmTile->AddSample(cameraSample.pFilm, L, rayWeight);

                    // Free _MemoryArena_ memory from computing image sample
                    // value
                    arena.Reset();
                } while (tileSampler->StartNextSample());
            }

            // Merge image tile into _Film_
            camera->film->MergeFilmTile(std::move(filmTile));
            reporter.Update();
        }, nTiles);
        reporter.Done();
    }

    // Save final image after rendering
    camera->film->WriteImage();
}
Пример #6
0
void BDPTIntegrator::Render(const Scene &scene) {
    ProfilePhase p(Prof::IntegratorRender);
    // Compute _lightDistr_ for sampling lights proportional to power
    std::unique_ptr<Distribution1D> lightDistr =
        ComputeLightPowerDistribution(scene);

    // Partition the image into tiles
    Film *film = camera->film;
    const Bounds2i sampleBounds = film->GetSampleBounds();
    const Vector2i sampleExtent = sampleBounds.Diagonal();
    const int tileSize = 16;
    const int nXTiles = (sampleExtent.x + tileSize - 1) / tileSize;
    const int nYTiles = (sampleExtent.y + tileSize - 1) / tileSize;
    ProgressReporter reporter(nXTiles * nYTiles, "Rendering");

    // Allocate buffers for debug visualization
    const int bufferCount = (1 + maxDepth) * (6 + maxDepth) / 2;
    std::vector<std::unique_ptr<Film>> weightFilms(bufferCount);
    if (visualizeStrategies || visualizeWeights) {
        for (int depth = 0; depth <= maxDepth; ++depth) {
            for (int s = 0; s <= depth + 2; ++s) {
                int t = depth + 2 - s;
                if (t == 0 || (s == 1 && t == 1)) continue;

                std::string filename =
                    StringPrintf("bdpt_d%02i_s%02i_t%02i.exr", depth, s, t);

                weightFilms[BufferIndex(s, t)] = std::unique_ptr<Film>(new Film(
                    film->fullResolution,
                    Bounds2f(Point2f(0, 0), Point2f(1, 1)),
                    std::unique_ptr<Filter>(CreateBoxFilter(ParamSet())),
                    film->diagonal * 1000, filename, 1.f));
            }
        }
    }

    // Render and write the output image to disk
    if (scene.lights.size() > 0) {
        StatTimer timer(&renderingTime);
        ParallelFor2D([&](const Point2i tile) {
            // Render a single tile using BDPT
            MemoryArena arena;
            int seed = tile.y * nXTiles + tile.x;
            std::unique_ptr<Sampler> tileSampler = sampler->Clone(seed);
            int x0 = sampleBounds.pMin.x + tile.x * tileSize;
            int x1 = std::min(x0 + tileSize, sampleBounds.pMax.x);
            int y0 = sampleBounds.pMin.y + tile.y * tileSize;
            int y1 = std::min(y0 + tileSize, sampleBounds.pMax.y);
            Bounds2i tileBounds(Point2i(x0, y0), Point2i(x1, y1));
            std::unique_ptr<FilmTile> filmTile =
                camera->film->GetFilmTile(tileBounds);
            for (Point2i pPixel : tileBounds) {
                tileSampler->StartPixel(pPixel);
                if (!InsideExclusive(pPixel, pixelBounds))
                    continue;
                do {
                    // Generate a single sample using BDPT
                    Point2f pFilm = (Point2f)pPixel + tileSampler->Get2D();

                    // Trace the camera and light subpaths
                    Vertex *cameraVertices = arena.Alloc<Vertex>(maxDepth + 2);
                    Vertex *lightVertices = arena.Alloc<Vertex>(maxDepth + 1);
                    int nCamera = GenerateCameraSubpath(
                        scene, *tileSampler, arena, maxDepth + 2, *camera,
                        pFilm, cameraVertices);
                    int nLight = GenerateLightSubpath(
                        scene, *tileSampler, arena, maxDepth + 1,
                        cameraVertices[0].time(), *lightDistr, lightVertices);

                    // Execute all BDPT connection strategies
                    Spectrum L(0.f);
                    for (int t = 1; t <= nCamera; ++t) {
                        for (int s = 0; s <= nLight; ++s) {
                            int depth = t + s - 2;
                            if ((s == 1 && t == 1) || depth < 0 ||
                                depth > maxDepth)
                                continue;
                            // Execute the $(s, t)$ connection strategy and
                            // update _L_
                            Point2f pFilmNew = pFilm;
                            Float misWeight = 0.f;
                            Spectrum Lpath = ConnectBDPT(
                                scene, lightVertices, cameraVertices, s, t,
                                *lightDistr, *camera, *tileSampler, &pFilmNew,
                                &misWeight);
                            if (visualizeStrategies || visualizeWeights) {
                                Spectrum value;
                                if (visualizeStrategies)
                                    value =
                                        misWeight == 0 ? 0 : Lpath / misWeight;
                                if (visualizeWeights) value = Lpath;
                                weightFilms[BufferIndex(s, t)]->AddSplat(
                                    pFilmNew, value);
                            }
                            if (t != 1)
                                L += Lpath;
                            else
                                film->AddSplat(pFilmNew, Lpath);
                        }
                    }
                    filmTile->AddSample(pFilm, L);
                    arena.Reset();
                } while (tileSampler->StartNextSample());
            }
            film->MergeFilmTile(std::move(filmTile));
            reporter.Update();
        }, Point2i(nXTiles, nYTiles));
        reporter.Done();
    }
    film->WriteImage(1.0f / sampler->samplesPerPixel);

    // Write buffers for debug visualization
    if (visualizeStrategies || visualizeWeights) {
        const Float invSampleCount = 1.0f / sampler->samplesPerPixel;
        for (size_t i = 0; i < weightFilms.size(); ++i)
            if (weightFilms[i]) weightFilms[i]->WriteImage(invSampleCount);
    }
}
Пример #7
0
void BDPTIntegrator::Render(const Scene &scene) {
    // Compute _lightDistr_ for sampling lights proportional to power
    std::unique_ptr<Distribution1D> lightDistr(ComputeLightSamplingCDF(scene));

    // Partition the image into buckets
    Film *film = camera->film;
    const Bounds2i sampleBounds = film->GetSampleBounds();
    const Vector2i sampleExtent = sampleBounds.Diagonal();
    const int bucketSize = 16;
    const int nXBuckets = (sampleExtent.x + bucketSize - 1) / bucketSize;
    const int nYBuckets = (sampleExtent.y + bucketSize - 1) / bucketSize;
    ProgressReporter reporter(nXBuckets * nYBuckets, "Rendering");

    // Allocate buffers for debug visualization
    const int bufferCount = (1 + maxdepth) * (6 + maxdepth) / 2;
    std::vector<std::unique_ptr<Film>> films(bufferCount);
    if (visualize_strategies || visualize_weights) {
        for (int depth = 0; depth <= maxdepth; ++depth) {
            for (int s = 0; s <= depth + 2; ++s) {
                int t = depth + 2 - s;
                if (t == 0 || (s == 1 && t == 1)) continue;

                char filename[32];
                snprintf(filename, sizeof(filename),
                         "bdpt_d%02i_s%02i_t%02i.exr", depth, s, t);

                films[BufferIndex(s, t)] = std::unique_ptr<Film>(new Film(
                    film->fullResolution,
                    Bounds2f(Point2f(0, 0), Point2f(1, 1)),
                    CreateBoxFilter(ParamSet()),
                    film->diagonal * 1000,  // XXX what does this parameter
                                            // mean? Why the multiplication?
                    filename, 1.f, 2.2f));
            }
        }
    }

    // Render and write the output image to disk
    {
        StatTimer timer(&renderingTime);
        ParallelFor([&](const Point2i bucket) {
            // Render a single bucket using BDPT
            MemoryArena arena;
            int seed = bucket.y * nXBuckets + bucket.x;
            std::unique_ptr<Sampler> bucketSampler = sampler->Clone(seed);
            int x0 = sampleBounds.pMin.x + bucket.x * bucketSize;
            int x1 = std::min(x0 + bucketSize, sampleBounds.pMax.x);
            int y0 = sampleBounds.pMin.y + bucket.y * bucketSize;
            int y1 = std::min(y0 + bucketSize, sampleBounds.pMax.y);
            Bounds2i bucketBounds(Point2i(x0, y0), Point2i(x1, y1));
            std::unique_ptr<FilmTile> filmTile =
                camera->film->GetFilmTile(bucketBounds);
            for (Point2i pixel : bucketBounds) {
                bucketSampler->StartPixel(pixel);
                do {
                    // Generate a single sample using BDPT
                    Point2f rasterPos((Float)pixel.x, (Float)pixel.y);
                    rasterPos += bucketSampler->Get2D();

                    // Trace the light and camera subpaths
                    Vertex *cameraSubpath =
                        (Vertex *)arena.Alloc<Vertex>(maxdepth + 2);
                    Vertex *lightSubpath =
                        (Vertex *)arena.Alloc<Vertex>(maxdepth + 1);
                    int nCamera = GenerateCameraSubpath(
                        scene, *bucketSampler, arena, maxdepth + 2, *camera,
                        rasterPos, cameraSubpath);
                    int nLight = GenerateLightSubpath(
                        scene, *bucketSampler, arena, maxdepth + 1,
                        cameraSubpath[0].GetTime(), *lightDistr, lightSubpath);

                    // Execute all connection strategies
                    Spectrum pixelWeight(0.f);
                    for (int t = 1; t <= nCamera; ++t) {
                        for (int s = 0; s <= nLight; ++s) {
                            int depth = t + s - 2;
                            if ((s == 1 && t == 1) || depth < 0 ||
                                depth > maxdepth)
                                continue;
                            // Execute the $(s, t)$ connection strategy
                            Point2f finalRasterPos = rasterPos;
                            Float misWeight = 0.f;
                            Spectrum weight = ConnectBDPT(
                                scene, lightSubpath, cameraSubpath, s, t,
                                *lightDistr, *camera, *bucketSampler,
                                &finalRasterPos, &misWeight);
                            if (visualize_strategies || visualize_weights) {
                                Spectrum value(1.0f);
                                if (visualize_strategies) value *= weight;
                                if (visualize_weights) value *= misWeight;
                                films[BufferIndex(s, t)]->Splat(finalRasterPos,
                                                                value);
                            }
                            if (t != 1)
                                pixelWeight += weight * misWeight;
                            else
                                film->Splat(finalRasterPos, weight * misWeight);
                        }
                    }
                    filmTile->AddSample(rasterPos, pixelWeight, 1.0f);
                    arena.Reset();
                } while (bucketSampler->StartNextSample());
            }
            film->MergeFilmTile(std::move(filmTile));
            reporter.Update();
        }, Point2i(nXBuckets, nYBuckets));
        reporter.Done();
    }
    film->WriteImage(1.0f / sampler->samplesPerPixel);

    // Write buffers for debug visualization
    if (visualize_strategies || visualize_weights) {
        const Float invSampleCount = 1.0f / sampler->samplesPerPixel;
        for (size_t i = 0; i < films.size(); ++i) {
            if (films[i]) films[i]->WriteImage(invSampleCount);
        }
    }
}