CG5 - 相机成像

07 Jul 2015

0、概述

经过前面诸多的准备工作,构建了物体,变换了坐标,最后还需要在渲染器中放入一个相机才能将物体显示出来。

相机的实现存放在 Camera.h 中,虽然只有一个文件,但是相机负责的工作却相当多。

首先回顾一下上一节最后流水线的结构:

pipeline2

在我们用眼睛观察周围时,有许多因素影响我们看到的图像:站的位置、看的方向、眼睛的大小等。在相机模型中,同样需要具备这些要素:

1
2
3
4
5
6
7
8
9
10
11
12
struct Camera
{
    ...
    Point4D pos;    // 位置
    Vector4D dir;   // 方向
    float viewport_width, viewport_height, \    // 视平面宽度,高度
    viewport_cx, viewport_cy;                   // 视平面 x 轴, y 轴
    float aspect_ratio;                         // 视角
    ...
    void build_Euler(int cam_rot_seq);          // 欧拉相机
    void build_UVN(int mode);                   // UVN 相机
};

在实现中,可以构建欧拉相机和 UVN 相机两种模型,本节仅讲述欧拉相机。

1、面

从 Pipeline.h 中生成了同一世界坐标系中多个面与顶点组合成的渲染列表,相机位于相机的坐标系,想要显示世界坐标系中的物体,有两种方法实现:

1)将相机坐标系变换到世界坐标系:显示时,需要计算相机的坐标、空间中物体与相机的相互关系,判断物体可见性并显示物体;

2)将世界坐标系变换到相机坐标系:显示时,需要变换空间中的物体到相机坐标系,判断物体可见性并显示物体。

软件渲染器计算物体时,需要大量的浮点运算,若将相机变换到世界坐标系,那么在计算空间中物体与相机的相互关系时,不利于归一化处理,会产生许多额外的浮点运算;若将世界坐标系中的物体变换到相机坐标系,则可通过对相机模型的简化,实现运算简化。

在实现中,可以将单一物体转化至相机坐标系,也可以将渲染列表变换到坐标系,依赖于 mcam 矩阵完成变换。

1
2
void from_World(PObject4D obj);             // 将物体变换到相机坐标系
void from_World(PRenderList4D rlist);       // 将渲染列表变换到相机坐标系

转换后的坐标依然是 3D 坐标,仍不能显示出来,还需要我们将 3D 坐标投影到 2D Screen 上。

1
2
3
4
void to_Perspective(PObject4D obj);         // 投影物体的顶点到 2D Screen 上
void to_Perspective(PRenderList4D rlist);   // 投影渲染列表的顶点到相机坐标系
void to_Screen(PObject4D obj);              // 将 2D Screen 上的顶点坐标映射到显示窗口
void to_Screen(PRenderList4D rlist);        //  2D Screen 上的顶点坐标映射到显示窗口

circleDemo

在此,利用目前所实现的接口,将一个 3D 正方体显示出来,代码在此(此后将不再贴完整代码,仅附上代码片段或链接)。

cubeDemo1

大多数情况下,并不需要关注物体的背面,因为相机实际观察到的永远是遮挡在最前方的物体。这部分只需要求相机方向与物体各个面法向量的夹角即可,当一个面的法向量与相机方向相同时(即背向相机),需要将这个面标记为背面,这时候需要为面数据结构(Poly4D、PolyList4D)创建一个新的数据成员 state 来存储面的状态信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
inline void Camera::remove_Backfaces(PObject4D obj)
{
    ...
    Vector4D u(&obj->vlist_trans[vert0], &obj->vlist_trans[vert1]);
    Vector4D v(&obj->vlist_trans[vert0], &obj->vlist_trans[vert2]);
    Vector4D n(u.cross(&v));
    Vector4D view(&obj->vlist_trans[vert0], &pos);
    float dp = n.dot(&view);
    if (dp <= 0.0)
    {
        curr_poly->state |= POLY4D_STATE_BACKFACE;
    }
}

cubeDemo2

2、物体剔除

在对物体进行一系列处理前,需要判断物体是否在有效视线区域内,若物体在视区外,则标记后无需进行多余的处理。物体有多判断方法,我的实现中使用了相对简单的包围球检测:每个物体存放其中心坐标和最大半径,根据球的外径进行判断。

经过剔除后,仅保留途中橙色的物体。

cull

3、透视变换

透视变化可以理解成归一化处理,用通俗的话说就是按比例缩放至同一大小的平面,《大师技巧》图6.35中描绘了透视变换将视景体变成长方形,甚妙。在这里我从另外一个角度贴出透视变换的理解图。

perspective

4、成像

将透支生成的坐标,按比例放大到显示窗口中。

1
2
3
4
5
6
7
8
9
10
11
12
inline void Camera::to_Screen(PRenderList4D rlist)
{
    ...
    float alpha = (float)0.5 * viewport_width;
    float beta = (float)0.5 * viewport_height;

    for (int vertex = 0; vertex < 3; vertex++)
    {
        curr_poly->tvlist[vertex].x = alpha + alpha * curr_poly->tvlist[vertex].x;
        curr_poly->tvlist[vertex].y = beta + beta * curr_poly->tvlist[vertex].y;
    }
}

至此,3D 流水线有了进一步的变化。

pipeline3


如果你发现我文章中的问题或希望与我交流,可以给我发邮件:plinux@qq.com

如果你读后有些许收获,也可以到我的 Github 点赞支持

© 2015 plinx