PBRT阅读:第六章 相机模型

http://www.openeda.net/forum.php?mod=viewthread&tid=4658

第六章 相机模型

在第一章里,我们描述了计算机图形学中常用的针孔相机模型。这种模型易于描述和模拟,但有严重的缺陷。例如,用针孔相机所拍摄出的所有景物都在锐聚焦区(sharp focus)中,所得到的图像就像是计算机生成的。为了使图像更具真实感,有必要更好地模拟真实世界中成像系统的特性。

跟第三章中Shape类一样,在pbrt中,相机也是用一个抽象类来表达的。本章介绍Camera类及其唯一的函数:Camera::GenerateRay()。该函数计算图像平面上任一点所对应的世界空间中的光线。生成光线的方式因成像模型的不同而不同,所以pbrt中的相机可以对同一个三维场景生成不同的图像。这里我们要介绍Camera接口的几个实现,每个实现的光线生成方法都不同。

6.1 相机模型

Camera抽象基类包含相机的通用选项并定义了所有相机实现的共同的接口。

<Camera Declarations> =
    class COREDLL Camera {
    public:
        <Camera Interface>
        <Camera Public Data>
    protected:
        <Camera Protected Data>
    };

Camera的子类必须实现函数Camera::GenerateRay(),该函数根据给定的图像采样点生成光线。注意Camera对所返回的光线的方向向量进行了正规化,因为系统的许多其它部分需要正规化的光线方向。

这个函数还返回一个浮点数的权值,用来控制光沿着所生成的光线到达胶片平面上的效果。大多数情况下该函数返回1,但那些模拟真实物理透镜系统的相机需要根据虚拟透镜系统的光学和几何性质来设置该值。

<Camera Interface> =
    virtual float GenerateRay(const Sample &sample, Ray *ray) const = 0;

Camera基类的构造器需要几个通用的相机参数,其中包括把相机放置到场景中的变换,在相机空间中决定可见区域的近裁剪面和远裁剪面。任何处于近裁剪面之前或远裁剪面之后的几何体素都不会出现在最终的图像中。

真实相机有一个可以快速打开和关闭的快门对胶片进行曝光。这个曝光时间所产生的效果之一就是运动模糊:在曝光的时候移动的物体会有模糊效果。虽然当前pbrt并不支持运动模糊效果,但是每个Ray对象有一个时间值与之对应,所以可以很容易地加上这个效果。为此,Camera存放一个快门打开时间值和一个快门关闭时间值。

<Camera Protected Data> =
    Transform WorldToCamera, CameraToWorld;
    float ClipHither, ClipYon;   // Hither: 近的; Yon:远的
    float ShutterOpen, ShutterClose;

Camera还包括Film类的一个实例,用来表示要计算的最后的图像结果。我们将在第八章中介绍Film。

<Camera Public Data> =
    Film *film;

Camera的实现必须要把这些值传给Camera构造器。下面只列出了构造器的原型:

<Camera Interface> +=
    Camera(const Transform &world2cam, float hither, float yon, 
            float sopen, float sclose, Film *film);

6.1.1 相机坐标空间

到目前为止我们用到了两个重要的坐标空间:物体空间和世界空间。现在我们要介绍跟相机模型和成像过程相关的四个有用的坐标空间(如图):

pbrt-06-1.jpg 
·    物体空间: 这是定义几何体素所用的坐标系系统。例如,pbrt中的球面就是定义在物体空间,并且球心就在空间的原点。
·    世界空间:每个体素有其自己的物体空间,所有的场景中的物体都要被放置在一个单一的世界空间。每个体素有一个物体/空间变换用来确定它在世界空间的位置。世界空间是标准坐标系,也是定义其它所有空间的依据。
·    相机空间:我们把具有特定观察方向和向上方向(指的是up向量)的虚拟相机放置在世界空间的某个点上。这就定义了一个原点位于相机位置的新坐标系。该坐标系的z轴被映射为观察方向,y轴被映射为向上方向。这个空间有助于我们判断那个物体对相机而言是否可见。例如,如果物体在相机空间中的包围盒全部落在z=0平面的后面,则物体不可见。
·    屏幕空间:屏幕空间定义在图像平面上。相机把相机空间中的物体投影到图像平面上;在屏幕窗口内的那一部分在图像中是可见的。屏幕空间中深度z值的范围是0到1,0和1分别对应于近裁剪面和远裁剪面。注意虽然我们称之为“屏幕”空间,但它仍是三维空间,因为z值是有意义的。
·    正规化设备空间(Normalized device coordinate, NDC):这是图像渲染所用的坐标系。在x,y平面上,其空间范围是从(0,0)到(1,1),并且(0,0)对应于图像的左上角。深度值z跟屏幕空间中的值相同。从屏幕空间到NDC空间是一个简单的线性变换。

所有相机对象存放一个世界空间到相机空间的变换,它用来把场景中的体素变换到相机空间。相机空间的原点就是相机的位置,相机的方向跟相机空间的z轴保持一致。下节所介绍的投影相机用4x4矩阵进行空间之间的变换,但是具有特殊图像效果的相机并不一定能用矩阵来表示所有这些变换。


6.2 投影相机模型

三维计算机图形学的基本问题之一就是三维观察问题:即如何把三维场景投影到要显示的二维图像。大多数经典的解决方法是用4x4投影变换矩阵。所以,我们将介绍一个使用投影矩阵的相机类,并以此为基础定义两个相机模型。第一个模型实现了正交投影,另一个实现了透视投影,这两个投影模型都很经典并且应用广泛。

<Camera Declarations> +=
    class COREDLL ProjectiveCamera : public Camera {
    public:
        < ProjectiveCamera Public Methods>
    protected:
        < ProjectiveCamera Protected Data>
    };

除了Camera基类所带的参数外,ProjectiveCamera还有投影变换矩阵,图像的屏幕空间范围,关于景深的参数。在本节的最后,将会介绍景深,它是用来模拟真实透镜系统中不在焦点范围内的物体的模糊效果的。

<Camera Methods Definitions> =
    ProjectiveCamera:: ProjectiveCamera(const Transform &w2c,
        const Transform &proj, const float Screen[4],
        float hither, float yon, float sopen,
        float sclose, float lensr, float focald, Film *f)
    : Camera(w2c, hither, yon, sopen, sclose, f) {
        <Initialize depth of field parameters>
        <Compute projective camera transformations>
    }

ProjectiveCamera的子类要把投影矩阵传给基类的构造器。这个变换给出了相机到屏幕的变换;构造器以此为基础,计算出其它的变换矩阵。

<Compute projective camera transformations> =
    CameraToScreen = proj;
    WorldToScreen = CameraToScreen * WorldToCamera;
    <Compute projective camera screen transformations>
    RasterToCamera = CameraToScreen.GetInverse() * RasterToScreen;

<ProjectiveCamera Protected Data> =
    Transform CameraToScreen, WorldToScreen, RasterToCamera;

其中稍稍费事的变换是计算屏幕到光栅(screen-to-raster)的投影。在下面的代码中,注意构造变换的方式(代码要从下往上读):首先,我们从屏幕空间中的一个点开始,把它平移使得屏幕的左上角落在原点上,然后使用屏幕宽度的倒数和屏幕高度的倒数进行比例变换,这样就得到了值在0到1之间的x,y坐标值(NDC空间中的坐标)。最后,再把结果按照光栅分辨率进行比例变换。其中一个重要细节是y坐标被反转了,因为在屏幕坐标中y是递增的,而光栅坐标的y则相反。

<Compute projective camera screen transformations> =
    ScreenToRaster = Scale(float(film->xResolution),
                float(film->yResolution), 1.f) *
            Scale(1.f/ (Screen[1] - Screen[0]),
                1.f/ (Screen[2] - Screen[3]), 1.f) *
            Translate(Vector(-Screen[0], - Screen[3], 0.f));
    RasterToScreen = ScreenToRaster.GetInverse();

<ProjectiveCamera Protected Data> +=
    Transform ScreenToRaster, RasterToScreen;

6.2.1 正交投影相机

<OrthographicCamera Declarations> =
    class OrthoCamera : public ProjectiveCamera {
    public:
        <OrthoCamera Public Methods>
    };

正交投影相机的基础是正交投影矩阵。正交投影变换用一个长方体来取景,并把场景投影到这个长方体的前面。这个投影不会有透视收缩效果(远些的物体在图像平面上要小一些),因为它保证平行线在变换后仍然保持平行,也就使得物体之间的相对距离在变换后保持不变。(如图,正交观察体是一个相机空间中和轴对齐的长方体,其中平面z=hither是长方体的前面,也是投影所要用到的平面)。
pbrt-06-2.jpg 
正交投影相机的构造器利用函数Orthographic()还生成正交变换矩阵。

<OrthographicCamera Definitions> =
    OrthoCamera::OrthoCamera(const Transform &world2cam,
        const float Screen[4], float hither, float yon,
        float sopen, float sclose, float lensr,
        float focald, Film *f)
    : ProjectiveCamera(world2cam, Orthographic(hither, yon),
            Screen, hither, yon, sopen, sclose,
            lensr, focald, f) {
    }

正交观察变换保持x,y坐标不变,但把z值映射到0(近裁剪面)和1(远裁剪面)之间。为此,我们先要沿z轴平移场景,使得近裁剪面跟平面z=0重合,然后整个场景在按z方向进行比例变换,使得远裁剪面被映射到平面z=1上。这两个变换的复合变换就是所要求的变换。

<Transform Method Definitions> +=
    Transform COREDLL Orthographic(float znear, float zfar) {
        return Scale(1.f, 1.f, 1.f/(zfar-znear)) * 
            Translate(Vector(0.f, 0.f, -znear));


现在我们可以过一遍把光栅空间中的采样点转换成相机光线的代码。相机采样的Sample::imageX和Sample::imageY是图像平面上的x,y光栅空间坐标(Sample结构详见第7章)。转换过程如图:

pbrt-06-3.jpg 

首先,要把光栅空间中的采样点变换到相机空间中的一个点,就得到了在近裁剪面上的一个点,也就是光线的原点。因为相机空间观察方向是沿z轴方向的,在相机空间中光线方向是(0,0,1)。然后设置光线的maxt值(设置为两个裁剪面的距离),以便能够略去在远裁剪面之后的交点。最后,把光线变换到世界空间中。

如果这个场景设置了景深,我们还要修改光线的原点和方向来模拟景深。在本节的最后会有对景深的更多的阐述。

<orthographicCamera Definitions> +=
    float OrthoCamera::GenerateRay(const Sample &sample, Ray *ray) const {
        <Generate raster and camera samples>
        ray->o = Pcamera;
        ray->d = Vector(0, 0, 1);
        <Set ray time value>
        <Modify ray for depth of field>
        ray->mint = 0;
        ray->maxt = ClipYon - ClipHither;
        ray->d = Normalize(ray->d);
        CameraToWorld(*ray, ray);
        return 1.f;
    }

因为已经有了所有的变换矩阵,就很容易把光栅采样点变换到相机空间:

<Generate raster and camera samples> =
    Point Pras(sample.imageX, sample.imageY, 0);
    Point Pcamera;
    RasterToCamera(Pras, &Pcamera);

Sample结构还包括了该光线被追踪时的时间,可用来实现运动模糊效果的功能。Sample的时间值处于0到1之间,以方便在快门打开时间和关闭时间的范围中进行插值计算。

6.2.2 透视相机

透视投影跟正交投影一样,也是把一个空间体(指的是以投影中心为顶点的透视四棱锥)投影到一个二维图像平面上。然而,它却有透视收缩效果:远些的物体在图像平面上的投影比近处相同大小的物体的投影要小一些。跟正交投影不同的是,透视投影并不保持距离和角度的相对大小不变,所以平行线的投影并不一定是平行的了。透视投影跟眼睛或相机镜头产生三维世界的图像的原理还是很接近的。

<PerspectiveCamera Declarations> =
    class PerspectiveCamera: public ProjectiveCamera {
    public:
        <PerspectiveCamera Public Methods>
    };

< PerspectiveCamera Method Definitions> =
    PerspectiveCamera:: PerspectiveCamera(const Transform &world2cam,
        const float Screen[4], float hither, float yon,
        float sopen, float sclose,
        float lensr,float focald, 
        float fov, Film *f)
    : ProjectiveCamera(world2cam, Perspective(fov, hither, yon),
            Screen, hither, yon, sopen, sclose,
            lensr, focald, f) {
    }

透视投影描述了对场景的透视观察。场景中的点被投影到观察平面z=1上,该平面跟位于z=0的虚拟相机的距离为单位长1。Perspective()函数用来计算这个变换矩阵。它的参数为视野角度(field-of-view angle)fov,和远/近裁剪面的距离n,f(如图)。
pbrt-06-4.jpg 
<Transform Method Definitions> +=
    COREDLL Transform Perspective(float fov, float n, float f) {
        <Compute basic perspective matrix>
        <Scale to canonical viewing volume>
    }

将该变换分成如下两步可以容易理解一些:

1. 把相机空间上的点p投影到观察平面。利用一些代数知识,我们可以知道在观察平面上的投影坐标x',y'分别是x,y除以点的z坐标值的结果。投影坐标z值被映射到0到1之间(0对应于近裁剪面,1对应于远裁剪面)。所要做的计算如下:

        x' = x /z
        y' = y/z
        z' = f(z - n) / z(f-n)

把这些计算归纳成一个使用齐次坐标的4x4矩阵,即为:
pbrt-06-5.jpg 
<Compute basic perspective matrix> =
    float inv_denom = 1.f / (f - n);
    Matrix4x4 *persp = 
        new Maxtrix(1, 0, 0, 0,
                0, 1, 0, 0,
                0, 0, f * inv_denom, -f*n*inv_denom,
                0, 0, 1, 0);

2. 用户提供的视野角度被用来做投影平面上(x,y)点的比例变换,使得在视野中的点被投影到[-1, 1]的坐标值范围中。对于正方形图像,x和y在屏幕空间的取值范围都是[-1,1]。对于非正方形图像,图像窄的那一方向的范围是[-1,1],而另一方向上的范围要相对大一些。

我们知道正切值是直角三角形的对边和邻边之比,而邻边长为1,所以对边长为tan(fov/2)。用这个长度的倒数进行比例变换就可以把视野的取值范围映射成[-1, 1]。

<Scale to canonical viewing volume> =
    float invTanAng = 1.f / tanf(Radians(fov) / 2.f);
    return Scale(invTanAng, invTanAng, 1) * Transform(persp);

在透视投影中,光线的起点在近裁剪面的采样位置上,其在相机空间的方向为从(0,0,0)到采样位置的向量。换句话说,光线的方向向量的各个分量就等于采样点的位置的各个分量。

跟OrthoCamera一样,光线的maxt值对应于远裁剪面的位置。

<PerspectiveCamera Method Definitions > +=
    float PerspectiveCamera::GenerateRay(const Sample &sample,
                        Ray *ray) const {
        <Generate raster and camera samples>
        ray->o = PCamera;
        ray->d = Vector(Pcamera.x, Pcamera.y, pcamera.z);
        <Set ray time value>
        <Modify ray for depth of field>
        ray->d = Normalize(ray->d);
        ray->mint = 0;
        ray->maxt = (ClipYon-ClipHither) / ray->d.z;
        CameraToWorld(*ray, ray);
        return 1.f;
    }

6.2.3 景深

真正的相机的透镜系统令光穿过一个有限大小的光圈并把光线聚焦在胶片平面上。因为光圈的面积有限,场景中的一个点投影到胶片空间上有可能变成一个区域,即模糊圈(circle of confusion)。相应地,场景中的一个区域在图像平面上变成一个点,这样就形成了模糊的图像。

模糊圈的大小受光圈半径和镜头与物体的距离的影响。焦距(focal distance)是镜头到可以产生零半径模糊圈的物体平面(the plane of objects)的距离。这些点在完美聚焦下非常清晰。在实际应用中,物体并不一定精确地处于聚焦平面上才会有清晰的效果;只要模糊圈大致上小于一个像素,物体就会像被聚焦的样子了。使得物体看上去被聚焦的(到镜头的)距离范围被称为镜头的景深。

ProjectiveCamera有两个关于景深的参数:一个是光圈的大小,一个是焦距。
<ProjectiveCamera Protected Data> +=
    float LensRadius, FocusDistance;

<Initialize depth of filed parameters> =
    LenRadius = lensr;
    FocusDistance = focald;

计算简单镜头的模糊圈并不复杂,其中只是运用了相似三角形原理和对镜头轮廓形状的近似。在光线追踪程序中,只要几行代码就可以搞定了。所要做的工作是:选择镜头上的一个点,找到起始于胶片平面上点的光线,该光线过镜头上所选择的点,并保证聚焦平面上的物体被聚焦。(如图)。不幸地是,为了适当地对镜头采样而得到平滑的景深效果,需要为每个图像像素追踪多条光线。
pbrt-06-6.jpg 
    
<Modify ray for depth of field> =
    if (LensRadius > 0.) {
        <Sample point on the lens>
        <Compute point on plane of focus>
        <Update ray for effect of lens>
    }

在第14章定义的CancentricSampleDisk()函数在区域[0,1]x[0,1]上取采样位置(u,v),并把它映射到二维单位圆盘上。为了把它转换成镜头上的一个点,需要把坐标值乘上镜头半径。Sample类用Sample::lensU, Sample::lensV来存放(u,v)采样参数。

<Sample point on lens> =
    float lensU, lensV;
    ConcentricsSampleDisk(sample.lensU, sample.lensV,
                    &lensU, &lensV);
    lensU *= LensRadius;
    lensV *= LensRadius;

光线的起点即是镜头上的这个点。现在我们确定光线的方向。我们可以使用Snell定律来求解这个方向,该定律描述了光线从一种介质(如空气)到另一介质(如玻璃)的折射性质,但是我们可以用更简单的方法。我们知道所有起始于图像采样点的光线穿过镜头后会聚于聚焦平面上的同一点上。还有,穿过镜头中心的光线并没有折射现象,所以,这个聚焦点就是这条穿过镜头中心的光线跟聚焦平面的交点。然后,我们把光线方向设置为从镜头上的点到该交点的向量。

对于这个简单的模型而言,聚焦平面跟z轴垂直,并且光线的起点在近裁剪面上,所以交点的t值为:

        t = (focalDistance  - hither) /dz

<Compute point on plane of focus>
    float ft = (FocalDistance - ClipHither ) / ray->d.z;
    Point Pfocus = (*ray)(ft);

现在我们可以做光线的初始化工作。我们把光线的起始点平移到镜头上的采样点,并把光线方向设置好,使得光线穿过聚焦平面上的点Pfocus。

<Update ray for effect of lens>
    ray->o.x += lensU;
    ray->o.y += lensV;
    ray->d = Pfocus - ray->o;

6.3 环境相机

跟扫描线算法以及其它的光栅化渲染算法比较,光线追踪算法可以非常容易地使用非常规的图像投影方法。对于如何决定把图像采样位置映射为光线方向,我们有很大的自由度,因为渲染算法不依赖于诸如“场景中的直线总是映射成图像中的直线”这样的性质。

在本节里,我们将描述一个相机模型,该模型追踪从场景中的一个点发出的所有方向上的光线,由此可以得到从一个点看到的所有可见物体的二维图像。考虑场景中一个围绕相机位置的球面,在球面上选择任意一点就得到所要追踪的光线的一个方向。我们用球面坐标将球面参数化,球面上的每个点都有一个坐标对(θ,Φ),其中θ在[0,π]之间,Φ在[0,2π]之间(第5.3.2节有关于球面坐标的更多的介绍)。这种图像类型很有用处,因为它表示了场景中一个点的所有的入射光。当我们在讨论环境光照(一种使用基于图像的光源的渲染技术)时,就会看到它的用处。

<EnvironmentCamera Declarations> =
                class EnvironmentCamera : public Camera {
                public:
                                < EnvironmentCamera Public Methods>
                private:
                                < EnvironmentCamera Private Data>
                };

注意EnvironmentCamera子类是从Camera类而不是ProjectiveCamera类继承来的。这是因为环境投射是非线性的,不能由单一的4x4矩阵表示。

这个相机所生成的光线有一个共同的起始点。为了提高效率,在构造器中我们计算出了相机在世界空间中的位置并暂存起来。

<EnvironmentCamera Private Data> =
                Point rayOrigin;
<EnvironmentCamera Method Definitions> =
                EnvironmentCamera:: EnvironmentCamera(const Transform &world2cam,
                                                                float hither, float yon, float sopen,
                                                                float sclose, Film *film)
                                : Camera(world2cam, hither, yon, sopen, sclose, film) {
                                                rayOrigin = CameraToWorld(Point(0,0,0));
                                }

注意EnvironmentCamera也使用近裁剪面和远裁剪面来限定光线的参数t值。实际上,称它们为“裁剪球面”更确切,因为所有的光线起始于一点并向各个方向放射。

< EnvironmentCamera Method Definitions > +=
                float EnvironmentCamera::GenerateRay(const Sample &sample,
                                                                                Ray *ray) const {
                                ray->o = rayOrigin;
                                <Generate environment camera ray direction>
                                <Set ray time value>
                                ray->mint = ClipHither;
                                ray->maxt = ClipYon;
                                return 1.f;
                }

为了计算光线的(θ,Φ)坐标,我们要计算光栅图像采样位置的NDC坐标,并对之做比例变换,把它映射到(θ,Φ)的取值范围中。下一步是利用球面坐标公式计算出光线方向,最后把方向转换到世界空间中。

<Generate environment camera ray direction> =
                float theta = M_PI * sample.imageY / film->yResolution;
                float phi = 2  * M_PI * sample.imageX / film ->xResolution;
                Vector dir(sinf(theta) * cosf(phi), cosf(theta),
                                                sinf(theta) * sinf(phi));
                CameraToWorld(dir, &ray->d);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值