Physically Based Rendering From Theory To Implementation
翻译:https://pbr-book.org/4ed/Cameras_and_Film/Camera_Interface
总目录: Physically Based Rendering From Theory To Implementation - 基于物理的渲染从理论到实践第四版原书翻译
文章目录
5.1 Camera Interface 摄像头接口
Camera类使用常用的基于 TaggedPointer 的方法,根据相机的实际类型动态地将接口方法调用分派到正确的实现。(照例,本书这里不会介绍这些方法的实现。)Camera在base/ Camera .h
文件中定义。
class Camera : public TaggedPointer<PerspectiveCamera, OrthographicCamera,
SphericalCamera, RealisticCamera> {
public:
<<Camera Interface>>
};
相机必须实现的第一个方法是GenerateRay(),用于计算给定图像样本对应的光线。重要的是,要对返回射线的方向分量进行归一化,因为系统的许多其他部分将依赖于此行为。如果由于某种原因,给定的CameraSample没有有效的光线,那么应该取消设置pstd::optional的返回值。射线的 SampledWavelengths 作为一个非常量引用传递,这样相机就可以对镜头中的色散进行建模。在这种情况下,射线只跟踪单个波长的光,GenerateRay()方法将调用SampledWavelengths::TerminateSecondary()。
<<Camera Interface>>=
pstd::optional<CameraRay> GenerateRay(CameraSample sample,
SampledWavelengths &lambda) const;
传递给GenerateRay()的CameraSample结构保存了指定相机光线所需的所有样本值。它的pFilm成员给出了在胶片上生成的光线应该携带辐射度(radiance)的点。光线穿过的镜头上的点是pLens(对于包含镜头概念的相机),而 time 给出了光线应该在场景中采样的时间。如果相机本身处于运动中,则时间值决定在生成光线时使用什么相机位置。
最后,filterWeight成员变量是一个额外的比例因子,当光线的辐射度添加到存储在胶片上的图像时,会应用这个比例因子。它包括用于过滤每个像素点图像样本的重建滤波器。该主题将在5.4.3节和8.8节讨论。
// sampler.h
<<CameraSample Definition>>=
struct CameraSample {
Point2f pFilm;//在胶片上生成的光线应该携带辐射度(radiance)的点
Point2f pLens;//光线穿过的透镜上的点(如果有透镜)
Float time = 0;//光线应该在场景中采样的时间
Float filterWeight = 1;//一个额外的比例因子,包括用于过滤每个像素点图像样本的重建滤波器
};
由GenerateRay()返回的CameraRay结构包含光线和与其相关联的光谱权重。简单的相机模型将权重保留为默认值1,而更复杂的相机模型,如RealisticCamera,返回一个用于图像形成的辐射度建模的权重。(第5.4.1节提供了更多关于在后一种情况下如何计算和使用该权重的信息。)
// cameras.h
<<CameraRay Definition>>=
struct CameraRay {
Ray ray;
SampledSpectrum weight = SampledSpectrum(1);
};
相机还必须提供GenerateRayDifferential()的实现,它可以像GenerateRay()一样计算主光线,但也可以计算在胶片平面和方向上移动一个像素的对应光线。关于相机光线如何作为胶片上位置的函数而变化的信息,有助于给系统的其他部分一个概念,即特定相机光线的样本代表了多少胶片区域,这对于反走样纹理查找很有用。
<<Camera Interface>>+=
pstd::optional<CameraRayDifferential> GenerateRayDifferential(
CameraSample sample, SampledWavelengths &lambda) const;
GenerateRayDifferential()返回CameraRayDifferential结构的一个实例,它等价于CameraRay,只是它存储了一个 RayDifferential。
<<CameraRayDifferential Definition>>=
struct CameraRayDifferential {
RayDifferential ray;
SampledSpectrum weight = SampledSpectrum(1);
};
相机实现必须提供对其Film的访问,这允许系统的其他部分确定诸如输出图像的分辨率之类的事情。
<<Camera Interface>>+=
Film GetFilm() const;
就像现实世界的相机一样,pbrt的相机模型包含了一个快门的概念,它可以在短时间内打开,让胶片暴露在光线下。这种非零曝光时间的结果之一就是运动模糊:在曝光期间相对于相机运动的物体会变得模糊。时间也是可以用于点采样和蒙特卡洛积分的另一个因素:给定快门开启时间和快门关闭时间之间光线时间的适当分布,就可以计算出表现出运动模糊的图像。
因此,SampleTime()接口方法应该将范围 [ 0 , 1 ) [0,1) [0,1)内的均匀随机样本u映射到相机快门打开的时间。通常,它只是用于在快门开启和关闭时间之间进行线性插值。
<<Camera Interface>>+=
Float SampleTime(Float u) const;
最后一个接口方法允许相机实现在ImageMetadata类中设置字段,以指定与相机相关的转换矩阵。如果输出映像格式支持存储这类辅助信息,那么它将包含在写入磁盘的最终映像中。
<<Camera Interface>>+=
void InitMetadata(ImageMetadata *metadata) const;
完整代码:
camera.h
:
// Camera Declarations
struct CameraRay;
struct CameraRayDifferential;
struct CameraWiSample;
struct CameraSample;
class CameraTransform;
class PerspectiveCamera;
class OrthographicCamera;
class SphericalCamera;
class RealisticCamera;
// Camera Definition
class Camera : public TaggedPointer<PerspectiveCamera, OrthographicCamera,
SphericalCamera, RealisticCamera> {
public:
// Camera Interface
using TaggedPointer::TaggedPointer;
inline pstd::optional<CameraRay> GenerateRay(
CameraSample sample, SampledWavelengths &lambda) const;
pstd::optional<CameraRayDifferential> GenerateRayDifferential(
CameraSample sample, SampledWavelengths &lambda) const;
inline Film GetFilm() const;
inline Float SampleTime(Float u) const;
void InitMetadata(ImageMetadata *metadata) const;
};
} // namespace pbrt
cameras.h
:
// CameraRay Definition
struct CameraRay {
Ray ray;
SampledSpectrum weight = SampledSpectrum(1);
};
// CameraRayDifferential Definition
struct CameraRayDifferential {
RayDifferential ray;
SampledSpectrum weight = SampledSpectrum(1);
};
5.1.1 Camera Coordinate Spaces 相机坐标空间
在我们开始描述pbrt相机模型的实现之前,我们将定义它们使用的一些坐标空间。除了3.1节介绍的世界空间之外,我们现在将介绍四个额外的坐标空间,对象空间、相机空间、相机-世界空间和渲染空间。总而言之,我们有:
- 对象空间:这是定义几何图元的坐标系统。例如,pbrt中的球体被定义为以其对象空间的原点为中心。
- 世界空间:虽然每个基元都有自己的对象空间,但场景中的所有对象都是与单一的世界空间相关联的。从世界到对象的转换决定了每个对象在世界空间中的位置。世界空间是定义所有其他空间的标准框架。
- 摄像机空间:摄像机被放置在场景的某个世界空间点上,具有特定的观看方向和方向。这个相机定义了一个新的坐标系统,它的原点在相机的位置。这个坐标系的坐标z轴映射到观看方向,坐标y轴映射到向上。
- 相机世界空间:与相机空间一样,这个坐标系统的原点是相机的位置,但它保持了世界空间的方向(即,与相机空间不同,相机不一定向下看z轴)。
- 渲染空间:这是为了渲染而将场景转换到的坐标系统。在pbrt中,它可能是世界空间、相机空间或相机世界空间。
传统上,基于栅格化的渲染器在相机空间中进行大部分计算:三角形顶点在投影到屏幕上并栅格化之前,先从物体空间转换到相机空间。在这种情况下,相机空间是一个方便的空间,用于推理哪些对象可能是相机可见的。例如,如果一个物体的相机空间边界框完全在 z = 0 z = 0 z=0 平面后面(并且相机的视场宽度没有大于180度),则该物体将不可见。
相反,许多光线跟踪器(包括在此之前的所有版本的pbrt)在世界空间中渲染。当生成光线时,相机实现可能开始于相机空间,但它们将这些光线转换到世界空间,在世界空间中执行所有后续光线相交和阴影计算。这种方法的一个问题是,浮点数离原点越近的精度越高,离原点越远的精度越低。如果相机放置在远离原点的地方,可能没有足够的精度来准确地表示它正在查看的场景的一部分。
图5 - 1说明了在世界空间中渲染的精度问题。在图5.1(a)中,场景是按照原始场景规范中提供的摄像机和对象进行渲染的,恰好在世界空间中每个坐标的
±
10
\pm 10
±10 范围内。在图5 - 1(b)中,摄像机和场景在每个维度上都被平移了1,000,000个单位。原则上,两幅图像应该是相同的,但对于第二种视点,精度要低得多,在几何模型中浮点数的离散化是可见的。
图5 - 1: 远离原点的地方失去浮点精度的影响(a)按照最初的规定,该场景距离原点不超过10个单位。在世界空间中渲染场景会产生预期的图像。(b)如果场景和相机都从原点平移1,000,000个单位,并且场景在世界空间中渲染,那么表示场景的浮点精度会显著降低,导致这种糟糕的结果。©如果在相机世界空间中渲染转换后的场景,可以获得更高的精度并保留几何细节。然而,由于在表示相机位置方面的准确性损失,视点略有偏移。(模型由 Yasutoshi Mori 提供。)
在相机空间中的渲染自然为最接近相机的对象提供了最高的浮点精度。如果图5 - 1中的场景是在相机空间中渲染的,那么同时平移相机和场景几何形状1 000 000单位是没有效果的——平移会被取消。然而,光线跟踪在使用相机空间时存在一个问题。场景通常以主要特征与坐标轴对齐的方式建模(例如,考虑一个建筑模型,其中地板和天花板可能与平面对齐)。这些特征的轴向边界框在一维上退化,从而减少了它们的表面积。第7章将介绍的BVH这样的加速结构对这种边界框特别有效。反过来,如果相机相对于场景旋转,轴对齐的边界框在边界这些特征方面的效果会更差,渲染性能会受到影响:对于图5 - 1中的场景,渲染时间增加了27%。
使用相机-世界空间进行渲染可以做到两全:相机位于原点,场景被相应地转换。然而,旋转并没有应用于场景几何,因此为加速结构保留了良好的边界框。使用camera-world空间,渲染时间没有增加,并且保持了更高的精度,如图5 - 1©所示。CameraTransform类通过处理不同空间之间转换的细节来抽象用于渲染的特定坐标系统的选择。
class CameraTransform {
public:
<<CameraTransform Public Methods>>
private:
<<CameraTransform Private Members>>
};
相机实现必须使其CameraTransform可用于系统的其他部分,因此我们将在相机接口中添加一个方法。
class Camera : public TaggedPointer<PerspectiveCamera, OrthographicCamera,
SphericalCamera, RealisticCamera> {
public:
...
const CameraTransform &GetCameraTransform() const;
};
CameraTransform维护两个转换:一个是从相机空间到渲染空间,另一个是从渲染空间到世界空间。在pbrt中,后一种转换不能动画化;相机变换中的任何动画都会在第一个变换中保留。这确保了移动的摄像机不会导致场景中的静态几何图形变成动画,这反过来会损害性能。
class CameraTransform {
public:
<<CameraTransform Public Methods>>
private:
// <<CameraTransform Private Members>>
AnimatedTransform renderFromCamera;
Transform worldFromRender;
};
CameraTransform构造函数接受场景描述中指定的从相机到世界的转换,并将其分解为前面描述的两个转换。默认的渲染空间是camera-world,不过可以使用命令行选项覆盖这个选择。
CameraTransform::CameraTransform(const AnimatedTransform &worldFromCamera) {
switch (Options->renderingSpace) {
case RenderingCoordinateSystem::Camera: {
<<Compute worldFromRender for camera-space rendering>>
} case RenderingCoordinateSystem::CameraWorld: {
<<Compute worldFromRender for camera-world space rendering>>
} case RenderingCoordinateSystem::World: {
<<Compute worldFromRender for world-space rendering>>
}
}
<<Compute renderFromCamera transformation>>
}
对于相机空间渲染,worldFromRender应该使用world-from-camera转换,而render-from-camera转换应该使用identity转换,因为这两个坐标系统是等价的。但是,因为worldFromRender不能被动画化,所以实现在帧的中点进行world-from-camera变换,然后将相机变换中任何动画的效果折叠到renderFromCamera中。
CameraTransform::CameraTransform(const AnimatedTransform &worldFromCamera) {
switch (Options->renderingSpace) {
case RenderingCoordinateSystem::Camera: {
// <<Compute worldFromRender for camera-space rendering>>
Float tMid = (worldFromCamera.startTime + worldFromCamera.endTime) / 2;
worldFromRender = worldFromCamera.Interpolate(tMid);
break;
} case RenderingCoordinateSystem::CameraWorld: {
<<Compute worldFromRender for camera-world space rendering>>
} case RenderingCoordinateSystem::World: {
<<Compute worldFromRender for world-space rendering>>
}
}
<<Compute renderFromCamera transformation>>
}
对于相机-世界空间中的渲染的默认情况,从渲染世界的转换是通过转换到相机在帧的中点的位置来给出的。
CameraTransform::CameraTransform(const AnimatedTransform &worldFromCamera) {
switch (Options->renderingSpace) {
case RenderingCoordinateSystem::Camera: {
// <<Compute worldFromRender for camera-space rendering>>
Float tMid = (worldFromCamera.startTime + worldFromCamera.endTime) / 2;
worldFromRender = worldFromCamera.Interpolate(tMid);
break;
} case RenderingCoordinateSystem::CameraWorld: {
// <<Compute worldFromRender for camera-world space rendering>>
Float tMid = (worldFromCamera.startTime + worldFromCamera.endTime) / 2;
Point3f pCamera = worldFromCamera(Point3f(0, 0, 0), tMid);
worldFromRender = Translate(Vector3f(pCamera));
break;
} case RenderingCoordinateSystem::World: {
<<Compute worldFromRender for world-space rendering>>
}
}
<<Compute renderFromCamera transformation>>
}
对于世界空间渲染,worldFromRender是身份转换。
<<Compute worldFromRender for world-space rendering>>=
worldFromRender = Transform();
break;
一旦设置了worldFromRender, worldFromCamera中保留的任何转换都会被提取并存储在renderFromCamera中。
<<Compute renderFromCamera transformation>>=
Transform renderFromWorld = Inverse(worldFromRender);
Transform rfc[2] = { renderFromWorld * worldFromCamera.startTransform,
renderFromWorld * worldFromCamera.endTransform };
renderFromCamera = AnimatedTransform(rfc[0], worldFromCamera.startTime,
rfc[1], worldFromCamera.endTime);
CameraTransform类提供了许多重载的方法,名为RenderFromCamera(),、CameraFromRender()和RenderFromWorld(),用于在它管理的坐标系中转换点、向量、法线和光线。其他方法则直接返回相应的变换。这里不包括它们的简单实现。
5.1.2 The CameraBase Class
本章中所有的相机实现都有一些共同的功能,我们把这些功能分解成一个类CameraBase,它们都继承自CameraBase。CameraBase以及所有相机的实现都定义在文件cameras .h
和cameras .cpp
中。
<<CameraBase Definition>>=
class CameraBase {
public:
<<CameraBase Public Methods>>
protected:
<<CameraBase Protected Members>>
<<CameraBase Protected Methods>>
};
CameraBase构造函数接受各种适用于所有pbrt相机的参数:
- 其中最重要的是将相机放置在场景中的变换,它由CameraTransform表示,存储在CameraTransform成员变量中。
- 接下来是一对浮点值,表示相机快门打开和关闭的时间。
- 一个 Film 实例存储了最终的图像和模型的底片传感器。
- 最后是Medium实例,表示相机所在的散射介质,如果有的话(Medium将在第11.4节中描述)。
一个小结构将它们捆绑在一起,并有助于缩短相机构造函数的参数列表的长度。
<<CameraBaseParameters Definition>>=
struct CameraBaseParameters {
CameraTransform cameraTransform;
Float shutterOpen = 0, shutterClose = 1;
Film film;
Medium medium;
};
这里只包含构造函数的原型,因为它的实现只是将形参分配给相应的成员变量。
<<CameraBase Protected Methods>>=
CameraBase(CameraBaseParameters p);
<<CameraBase Protected Members>>=
CameraTransform cameraTransform;
Float shutterOpen, shutterClose;
Film film;
Medium medium;
CameraBase可以直接实现Camera接口所需的许多方法,从而省去了在继承它的Camera实现中重复实现这些方法的麻烦。
例如,访问器方法使Film和CameraTransform 可用。
<<CameraBase Public Methods>>=
Film GetFilm() const { return film; }
const CameraTransform &GetCameraTransform() const {
return cameraTransform;
}
SampleTime()方法是通过使用样本u在快门开启和关闭时间之间进行线性插值来实现的。
<<CameraBase Public Methods>>+=
Float SampleTime(Float u) const {
return Lerp(u, shutterOpen, shutterClose);
}
CameraBase提供了一个GenerateRayDifferential()方法,通过多次调用相机的GenerateRay()方法来计算光线差分。一个微妙之处在于,使用此方法的相机实现仍然必须自己执行相机GenerateRayDifferential()方法,但然后从他们的实现中调用此方法。(注意,这个方法的签名与那个方法不同。)相机将this指针作为相机参数传递,这允许它调用相机的GenerateRay()方法。这种额外的复杂性源于我们没有为camera接口使用虚函数,这意味着CameraBase类本身没有能力调用该方法,除非向它提供相机。
<<CameraBase Method Definitions>>=
pstd::optional<CameraRayDifferential>
CameraBase::GenerateRayDifferential(Camera camera,
CameraSample sample, SampledWavelengths &lambda) {
<<Generate regular camera ray cr for ray differential>>
<<Find camera ray after shifting one pixel in the x direction>>
<<Find camera ray after shifting one pixel in the y direction>>
<<Return approximate ray differential and weight>>
}
主射线是通过第一次调用GenerateRay()找到的。如果给定的样本没有有效的射线,那么也不可能有射线微分。
<<Generate regular camera ray cr for ray differential>>=
pstd::optional<CameraRay> cr = camera.GenerateRay(sample, lambda);
if (!cr) return {};
RayDifferential rd(cr->ray);
为了找到x光线差分,我们做了两次尝试:一次使用前向差分,另一次使用后向差分,差分仅为一个像素的几分之一。这两种方法都要尝试,因为真实感相机模型形成的图像边缘会出现渐晕——有时主光线是有效的,但在一个方向上的移动会超过镜头系统形成的图像。在这种情况下,尝试另一个方向可能会成功地生成一条射线。
<<Find camera ray after shifting one pixel in the direction>>=
pstd::optional<CameraRay> rx;
for (Float eps : {.05f, -.05f}) {
CameraSample sshift = sample;
sshift.pFilm.x += eps;
<<Try to generate ray with sshift and compute differential>>
}
如果可以生成辅助x光线,则通过差分,初始化对应的像素范围的差分。
<<Find camera ray after shifting one pixel in the direction>>=
pstd::optional<CameraRay> rx;
for (Float eps : {.05f, -.05f}) {
CameraSample sshift = sample;
sshift.pFilm.x += eps;
// <<Try to generate ray with sshift and compute differential>>
if (rx = camera.GenerateRay(sshift, lambda); rx) {
rd.rxOrigin = rd.o + (rx->ray.o - rd.o) / eps;
rd.rxDirection = rd.d + (rx->ray.d - rd.d) / eps;
break;
}
}
实现片段<<Find camera ray after shifting one pixel in the y direction>>与此类似,不包括在这里。
如果x和y都找到了有效的射线,我们可以继续并将hasDifferentials成员变量设置为true。否则,主射线仍然可以追踪,只是没有可用的微分。
<<Return approximate ray differential and weight>>=
rd.hasDifferentials = rx && ry;
return CameraRayDifferential{rd, cr->weight};
最后,为了子类的方便,CameraBase提供了各种使用CameraTransform的转换方法。这里我们只包含射线方法;其他的也是类似的。
<<CameraBase Protected Methods>>+=
Ray RenderFromCamera(const Ray &r) const {
return cameraTransform.RenderFromCamera(r);
}