PBRT笔记(1)——主循环、浮点误差

时间:2023-12-20 22:05:38

PBRT2与3之间的改动

  1. 增加了一个功能完备的BRDF模型,支持体积光照与重要性多重路径采样。
  2. 次表面散射,基于光线追踪技术,无需预处理。
  3. 解决浮点数四折五入的问题
  4. 光子映射
  5. 样本生成
  6. 第一章多了讲并行的东西

    看到第2页
渲染分块问题

对这个渲染任务过多的分块会影响性能。

场景的复杂性会对不同CPU核心的渲染速度产生影响。所以如果是分块数等于核心数,渲染完的核心会等待没渲染完的核心。

过小分块也是不科学的,因为处理核心问题有一定的开销。

pbrt里用的是16*16的方案

浮点类型

pbrt采用了FLOAT,这样浮点格式会根据宏进行调整

主循环
void SamplerIntegrator::Render(const Scene &scene) {
Preprocess(scene, *sampler);
//分块并行渲染图片 //计算tiles, _nTiles_,用于并行运算
Bounds2i sampleBounds = camera->film->GetSampleBounds();
Vector2i sampleExtent = sampleBounds.Diagonal();
//书中说16*16分块可以解决大部分情况
const int tileSize = 16;
//sampleExtent.x + tileSize - 1 整除考虑
Point2i nTiles((sampleExtent.x + tileSize - 1) / tileSize,
(sampleExtent.y + tileSize - 1) / tileSize);
ProgressReporter reporter(nTiles.x * nTiles.y, "Rendering");
{
ParallelFor2D([&](Point2i tile) {
// Render section of image corresponding to _tile_ //内存池,之后会给Li函数
MemoryArena arena; //给每个块分配一个Samper实例
int seed = tile.y * nTiles.x + 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));
LOG(INFO) << "Starting image tile " << tileBounds; //取得指定范围的图片块
std::unique_ptr<FilmTile> filmTile =
camera->film->GetFilmTile(tileBounds); //开始指定区域的渲染循环
for (Point2i pixel : tileBounds) {
{
ProfilePhase pp(Prof::StartPixel);
tileSampler->StartPixel(pixel);
} // Do this check after the StartPixel() call; this keeps
// the usage of RNG values from (most) Samplers that use
// RNGs consistent, which improves reproducability /
// debugging.
if (!InsideExclusive(pixel, pixelBounds))
continue; do {
//初始化相机采样,存储了时间以及镜头位置的采样值
CameraSample cameraSample =
tileSampler->GetCameraSample(pixel); //生成相机光线
//以及光线的pdf
RayDifferential ray;
Float rayWeight =
camera->GenerateRayDifferential(cameraSample, &ray);
ray.ScaleDifferentials(
1 / std::sqrt((Float)tileSampler->samplesPerPixel));
++nCameraRays; //估算当前相机光线辐射度
Spectrum L(0.f);
if (rayWeight > 0) L = Li(ray, scene, *tileSampler, arena); //对渲染出错误结果的处理,log相应信息
if (L.HasNaNs()) {
LOG(ERROR) << StringPrintf(
"Not-a-number radiance value returned "
"for pixel (%d, %d), sample %d. Setting to black.",
pixel.x, pixel.y,
(int)tileSampler->CurrentSampleNumber());
L = Spectrum(0.f);
} else if (L.y() < -1e-5) {
LOG(ERROR) << StringPrintf(
"Negative luminance value, %f, returned "
"for pixel (%d, %d), sample %d. Setting to black.",
L.y(), pixel.x, pixel.y,
(int)tileSampler->CurrentSampleNumber());
L = Spectrum(0.f);
} else if (std::isinf(L.y())) {
LOG(ERROR) << StringPrintf(
"Infinite luminance value returned "
"for pixel (%d, %d), sample %d. Setting to black.",
pixel.x, pixel.y,
(int)tileSampler->CurrentSampleNumber());
L = Spectrum(0.f);
}
VLOG(1) << "Camera sample: " << cameraSample << " -> ray: " <<
ray << " -> L = " << L; // Add camera ray's contribution to image
filmTile->AddSample(cameraSample.pFilm, L, rayWeight); //释放内存池
arena.Reset();
} while (tileSampler->StartNextSample());
}
LOG(INFO) << "Finished image tile " << tileBounds; //将图片块合并到图片上去
camera->film->MergeFilmTile(std::move(filmTile));
reporter.Update();
}, nTiles);
reporter.Done();
}
LOG(INFO) << "Rendering finished"; //保存图片到文件
camera->film->WriteImage();
}

并行相关问题

  1. 读取场景文件以及创建场景都是单线程的,获取场景信息因为不涉及到修改数据,所以可以无视。我们只需要关注修改内存数据的情况。
  2. 请不要在不同步的情况下修改数据
  3. 对应初始化可以考虑std::call_once函数
  4. 实用程序类MemoryArena(用于高性能临时内存分配)和RNG(伪随机数生成)也不适合多线程使用; 这些类存储在调用其方法时被修改的状态,并且相互排除的保护修改到其状态的开销相对于它们执行的计算量而言过多。 因此,在上面的SamplerIntegrator :: Render()方法的代码中,实现在堆栈上分配这些类的perthread实例。
  5. 每个线程需要各复制一个Sampler的实例,以保证线程安全
  6. 目前的计算机架构运算除法、平方根和三角函数是最慢的。加法与乘法相比之下要快10~50倍。所以我们可以减少这种数学运算数量来提高性能。例如我们可以提前计算1/v,之后再乘。而不是重复除以v。

Shape类

  1. Shape存储了Transform(正变换、逆变换)指针指针,这些指针都指向了一个智能指针,通过Transform池来进行内存空间管理。 (3.1.1)
  2. 全部Shape对象都采用了独立int32进行id管理(3.1.1)
  3. 在PBRT中的一些Shape支持通过texture的alpha通道颜色进行cutting away。(3.1.1)
  4. IntersectP()返回相交结果而不返回相交表面数据,Intersect()则会返回相交表面数据。(3.1.3)
  5. 为了使用面光源,所以会计算表面面积(面积模型)(3.1.4)
  6. PBRT不支持单面渲染(RayTracig from the ground 支持这个)(3.1.5)

sphere

使用一元二次方程求根很可能会因为浮点数舍入误差从而得到错误结果,一般来说,(-b)、sqrtdis 都是正数的话,-b+sqrtdis所得的正根一般都会是正确的,此时我们可以使用“维达定理”求得负根。

$ x_1x_2=\dfrac{c}{a} \Leftrightarrow x_2= \dfrac{c}{ax_1}$

关于舍入误差

static constexpr Float MaxFloat = std::numeric_limits<Float>::max();
static constexpr Float Infinity = std::numeric_limits<Float>::infinity();
std::numeric_limits<Float>::epsilon() * 0.5;
我们可以通过断言 !(x==x)来判断是否是NaN,也可以使用std::isnan。

光线追踪中的光线与物体求交函数的tmin值,可以帮助抵消因为浮点数舍入误差而造成的在物体内部相交的问题。较大的tmin值可以起到更好的效果,但是过大的tmin值,会使靠的比较近的面丢失部分反射与阴影效果。

不过PBRT告诉我们,可以通过一定的系统设计,让我们不再需要ray epsilon。(使用EFLoat类)

EFloat类通过重载计算符的方式,使用误差计算公式。使得EFLoat类之间的计算可以正确地积累或者抵消之前浮点运算的误差。

low = NextFloatDown(v - err);
high = NextFloatUp(v + err);

最新的代码直接计算出误差边界,而不是存储在float err变量。