欢迎来到NiceSpace!祝大家开心每一天!
  • C++
  • 图形学
3D图形学总结(四)—透视坐标变换

经过相机坐标变换后,相机位置已经变换到了原点处,朝向指向正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, 1) * 

= ( 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, y, z, 1) * 

= ( 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);
}

 

随机文章
python爬虫之爬取捧腹网段子 3D图形学总结(四)—透视坐标变换 3D图形学总结(三)—相机坐标转换(UVN相机) 3D图形学总结(六)—背面消除与物体剔除 四元数的一些整理
推荐文章