原文链接:http://www.nicemxp.com/articles/21
经过相机坐标变换后,相机位置已经变换到了原点处,朝向指向正Z轴,如图:
如图是左手坐标系下3D系统俯视图,相机视野为90度,视景体内物体的顶点需要投射到视平面上完成透视变换,如果我们知道了视距d可以很容易算出顶点在视平面投影的坐标,如图:
图示是3D系统的侧视图,YOZ平面下根据相似三角形定理可以很容易得到:
d / z0 = yp / y0 => yp = y0 / z0 * d
同理 d / z0 = xp / x0 => xp = x0 / z0 * d
终上可得视点位于(0, 0, 0),视平面为z=d时物体顶点(x, y, z)的投影变换为:
xper = d * x / z,yper = d * x / z
可以通过矩阵运算来完成变换:
= ( x, y, z, z/d)
将所有的分量都除以 z/d => (x*d/z, y*d/z, d, 1),我们不需要考虑z的值因为我们只需要x和y,我们称上面的矩阵为透视变换矩阵Tper:
因为我们一般取视平面的宽度为2,坐标范围为( -1,1),而且相机视野为90度,所以我们可以求得d值为1,此时变换矩阵为:
经过矩阵运算后得到的透视坐标x范围为(-1, 1),y范围为(-1, 1)
这种情况是相机视野为90度,视平面是方形的,并且屏幕/视口也是方形的,我们需要考虑一般情况,就是屏幕/视口不是方形的,需要引入宽高比,如果在透视变换过程中不考虑宽高比的问题,在后面的屏幕坐标变换就需要考虑了,如果都不作处理最后得到的图形会发生比例失真。
我们以屏幕/视口600x400为例,宽高比aspect_ratio为3:2,相机视野为θ,视平面为2 x 2/aspect_ratio(保证视平面和屏幕的宽高比一致)。如图:
因为视屏面宽度w = 2, 所以我们可以求得d = 1 / tan(θ / 2)
有了d的值我们可以进行透视坐标变换,顶点经过投影后x坐标范围为(-1, 1),y坐标范围为(-1/aspect_ratio, 1/aspect_ratio),但是这个透视坐标运算过程中是没考虑到宽高比的,这时我们将y分量乘以宽高比,这样y坐标范围(-1, 1)就是归一化的了,后续屏幕坐标变换过程中就不需要考虑宽高比的问题了。因此我们的变换公式如下:
xper = d * x / z,yper = d *aspect_ratio * x / z
透视变换矩阵Tper:
顶点(x, y, z)经过透视矩阵运算后:
= ( x*d, y*d*aspect_ratio, z, z)
我们将结果转换为其次坐标,所有分量除以z:
(x*d/z, y*d*aspect_ratio/z, 1, 1)
至此,透视坐标变换就完成了。
构建透视变换矩阵源码:
//点和向量四维
typedef struct VECTOR4D_TYP
{
union
{
float M[4];
struct
{
float x, y, z, w;
};
};
} VECTOR4D, POINT4D, *VECTOR4D_PTR, *POINT4D_PTR;
//4x4矩阵
typedef struct MATRIX4X4_TYP
{
union
{
float M[4][4];
struct
{
float M00, M01, M02, M03;
float M10, M11, M12, M13;
float M20, M21, M22, M23;
float M30, M31, M32, M33;
};
};
} MATRIX4X4, *MATRIX4X4_PTR;
//向量初始化
void VECTOR4D_INITXYZ(VECTOR4D_PTR vt, float x, float y, float z)
{
vt->x = x;
vt->y = y;
vt->z = z;
vt->w = 1;
}
//向量复制
void VECTOR4D_COPY(VECTOR4D_PTR res, VECTOR4D_PTR src)
{
res->x = src->x;
res->y = src->y;
res->z = src->z;
res->w = src->w;
}
//4x4设置单位矩阵
void Mat_IDENTITY_4X4(MATRIX4X4_PTR ma)
{
Mat_Init_4X4(ma, 1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
}
//相机欧拉角度转换
void Eu_Dir_Transform(VECTOR4D_PTR dir)
{
dir->x = (dir->x / 180)*PI;
dir->y = (dir->y / 180)*PI;
dir->z = (dir->z / 180)*PI;
dir->w = 1;
}
//相机结构
typedef struct CAM4DV1_TYP
{
int state;
int attr;
POINT4D pos; //相机在世界坐标中的位置
VECTOR4D dir; //欧拉角度或者UVN相机模型的注视方向
VECTOR4D u;
VECTOR4D v;
VECTOR4D n;
POINT4D target;
float view_dist;//视距
float fov; //水平方向和垂直方向视野
float near_clip_z;//近裁剪面
float far_clip_z;//远裁剪面
//上下左右裁剪面 略
float viewplane_width;//视平面宽度
float viewplane_height;//视平面高度
float viewport_width;//视口宽度
float viewport_heght;//视口高度
float viewport_center_x;//视口中心x
float viewport_center_y;//视口中心y
float aspect_radio; //宽高比
MATRIX4X4 mcam; //相机变换矩阵
MATRIX4X4 mper; //透视变换矩阵
MATRIX4X4 mscr; //屏幕变换矩阵
}CAM4DV1, *CAM4DV1_PTR;
//初始化相机机构
void init_CAM4DV1(CAM4DV1_PTR cam,
int cam_attr,//相机属性
POINT4D_PTR cam_pos,//相机位置
VECTOR4D_PTR cam_dir,//相机朝向
POINT4D_PTR cam_target, //uvn相机初始目标位置
float near_clip_z,//近裁剪面
float far_clip_z,//远裁剪面
float fov,//视野
float viewport_width,//视口宽度
float viewport_height)//视口高度
{
cam->attr = cam_attr;
VECTOR4D_COPY(&cam->pos, cam_pos);
//相机欧拉角转换为弧度
Eu_Dir_Transform(cam_dir);
VECTOR4D_COPY(&cam->dir, cam_dir);
//对于UVN相机
VECTOR4D_INITXYZ(&cam->u, 1, 0, 0); //设置为x轴方向
VECTOR4D_INITXYZ(&cam->v, 0, 1, 0); //设置为y轴方向
VECTOR4D_INITXYZ(&cam->n, 0, 0, 1); //设置为z轴方向
if (cam_target != NULL)
VECTOR4D_COPY(&cam->target, cam_target);
else
VECTOR4D_INITXYZ(&cam->target, 0, 0, 0);
cam->near_clip_z = near_clip_z;
cam->far_clip_z = far_clip_z;
cam->viewport_width = viewport_width;
cam->viewport_heght = viewport_height;
cam->viewport_center_x = (viewport_width - 1) / 2;
cam->viewport_center_y = (viewport_height - 1) / 2;
cam->aspect_radio = viewport_width / viewport_height;
//将所有变换矩阵设置为单位矩阵
Mat_IDENTITY_4X4(&cam->mcam);
Mat_IDENTITY_4X4(&cam->mper);
Mat_IDENTITY_4X4(&cam->mscr);
cam->fov = fov / 180 * PI;
//设置视平面大小
cam->viewplane_width = 2.0;
cam->viewplane_height = 2.0 / cam->aspect_radio;
//视距
cam->view_dist = (cam->viewplane_width / 2) / tan(cam->fov / 2);
}
//构建透视变换矩阵
void BuildCameraToPerspectMatrix(CAM4DV1_PTR cam)
{
Mat_Init_4X4(&cam->mper, cam->view_dist, 0, 0, 0,
0, cam->view_dist*cam->aspect_radio, 0, 0,
0, 0, 1, 1,
0, 0, 0, 0);
}