从零实现3D图像引擎12构建支持欧拉和UVN的相机系统Word格式.docx
《从零实现3D图像引擎12构建支持欧拉和UVN的相机系统Word格式.docx》由会员分享,可在线阅读,更多相关《从零实现3D图像引擎12构建支持欧拉和UVN的相机系统Word格式.docx(7页珍藏版)》请在冰豆网上搜索。
它使用三个相互垂直的向量来表示相机的朝向:
1)相机注视的向量N
2)相机的上方向向量V
3)相机的右方向向量U
如下图,是在世界坐标系下的UVN相机的向量表示:
UVN来表示方向有什么好处呢?
首先,我们如果想让相机跟踪某个点来拍摄的话,这种方式无疑要比每次去计算欧拉旋转角好很多。
比如第三人称视角的游戏,一般都是以玩家为注视点。
想象一下对于这种情况,能够很直观的想到去转多少欧拉角度吗。
。
这里有几点非常值得注意:
1)如果我们有了注视点,通过用注视点坐标减去相机世界坐标,就可以求得N。
(尚未归一化的)
2)如果我们已知上向量V,那么就可以通过N×
V求得右向量U。
3)我们如果把上向量V通过直角三角形分解成在nv平面和uv平面上的两个分量的话,可以发现在nv平面上的分量没有意义,因为相机仰头或低头的方向已经由向量N来决定了。
所以实质上向量V是决定相机是否倾斜的,所谓的倾斜是指让左耳靠向肩部的动作,而不是头部向左转头的动作。
4)而且即使给定的上向量V与向量N不垂直,也可以根据N×
V求得U,因为N和V可以构成一个平面了,自然可以求其法向量。
然后再通过U×
N反求V,而这个V则是垂直于U和N的真正的V,其实也就是nv平面上的分量为0的V了。
3.定义相机结构
[cpp]viewplaincopytypedefstructCAMERA_TYPE//相机{POINT4DWorldPos;
//相机在世界的坐标VECTOR4DDirection;
//相机的朝向,相机默认的朝向intType;
//相机类型CAMERA_TYPE_ELUER或CAMERA_TYPE_UVNVECTOR4DU,V,N;
//UVN相机的u,v,n向量POINT4DUVNTarget;
//UVN相机的目标点intUVNTargetNeedCompute;
//UVN相机的目标点是否需要根据朝向计算,1-是,0-否,UVNTarget已给定doubleViewDistance;
//视距doubleFOV;
//视野角度doubleNearZ;
//近裁剪距离doubleFarZ;
//远裁剪距离PLANE3DClipPlaneLeft,ClipPlaneRight,ClipPlaneUp,ClipPlaneDown;
//上下左右裁剪平面doubleViewPlaneWidth,ViewPlaneHeight;
//透视平面的宽和高doubleScreenWidth,ScreenHeight;
//屏幕宽高doubleScreenCenterX,ScreenCenterY;
//屏幕中心坐标doubleAspectRatio;
//宽高比MATRIX4X4MatrixCamera;
//相机变换矩阵MATRIX4X4MatrixProjection;
//透视投影变换矩阵MATRIX4X4MatrixScreen;
//屏幕变换矩阵}CAMERA,*CAMERA_PTR;
都有注释,没啥好说的了,都是上面介绍的参数,最后有3个矩阵缓存变换矩阵。
4.创建相机并计算需要计算的相机参数
创建相机的函数除了把参数赋值外,要计算下列东西:
宽高比、视距、各裁剪面。
1)求宽高比
就是屏幕宽除以高。
cam-&
gt;
AspectRatio=(double)screenWidth/(double)screenHeight;
2)求视距
求视距OA很简单,已知视平面宽度为2,所以AB=1。
tan(FOV/2)=AB:
AO
所以,AO=AB/tan(FOV/2)
3)求各裁剪面
我们知道可以用平面上一点和平面的法向量来表示平面,因为所有的裁剪面都过原点O,所以只要找到法向量即可。
找法向量最简单的方法就是在平面上找到另外两个点,然后就这两个点的叉乘,就可以得到法向量。
找哪两个点呢?
看下图,是找右裁剪面上的另外两点,这两个点真爽:
再复习一遍叉乘公式吧:
u×
v=&
lt;
uy*vz-vy*uz,-ux*vz+vx*uz,ux*vy-vx*uy&
把上面的两点分别作为u和v代入:
&
w*d/2+w*d/2,-w*d/2+w*d/2,-w*w/4-w*w/4&
计算一下:
w*d,0,-w*w&
,我们可以把各分量缩放1/w,得:
d,0,-w/2&
呵呵,法向量就搞定了。
下面的函数,负责填充Camera的参数,并且计算上面这三个参数:
[cpp]viewplaincopyvoid_CPPYIN_3DLib:
:
CameraCreate(CAMERA_PTRcam,inttype,POINT4D_PTRpos,VECTOR4D_PTRdir,POINT4D_PTRtarget,VECTOR4D_PTRv,intneedtarget,doublenearz,doublefarz,doublefov,doublescreenWidth,doublescreenHeight)//创建相机{//相机类型cam-&
Type=type;
//设置位置和朝向VectorCopy(&
amp;
(cam-&
WorldPos),pos);
VectorCopy(&
Direction),dir);
//设置UVN相机的目标点if(target!
=NULL){VectorCopy(&
UVNTarget),target);
}else{VectorCreate(&
UVNTarget),0,0,0);
}if(v!
V),v);
}cam-&
UVNTargetNeedCompute=needtarget;
//裁剪面和屏幕参数cam-&
NearZ=nearz;
cam-&
FarZ=farz;
ScreenWidth=screenWidth;
ScreenHeight=screenHeight;
ScreenCenterX=(screenWidth-1)/2;
ScreenCenterY=(screenHeight-1)/2;
FOV=fov;
ViewPlaneWidth=2.0;
ViewPlaneHeight=2.0/cam-&
AspectRatio;
//根据FOV和视平面大小计算dcam-&
ViewDistance=(0.5)*(cam-&
ViewPlaneWidth)/tan(AngelToRadian(fov/2));
//所有裁剪面都过原点POINT3Dpo;
VectorCreate(&
po,0,0,0);
//面法线VECTOR3Dvn;
if(fov==90.0){//右裁剪面VectorCreate(&
vn,1,0,-1);
PlaneCreate(&
ClipPlaneRight,&
po,&
vn,1);
//左裁剪面VectorCreate(&
vn,-1,0,-1);
ClipPlaneLeft,&
//上裁剪面VectorCreate(&
vn,0,1,-1);
ClipPlaneUp,&
//下裁剪面VectorCreate(&
vn,0,-1,-1);
ClipPlaneDown,&
}else{//如果视野不是90度,则在算某个裁剪面的法向量时,先去视平面上四个角上在该平面上的两个角作为该裁剪面上的两个向量,然后求叉乘,即可//下面的法向量vn直接使用了结果//右裁剪面VectorCreate(&
vn,cam-&
ViewDistance,0,-cam-&
ViewPlaneWidth/2.0);
vn,-cam-&
vn,0,cam-&
ViewDistance,-cam-&
vn,0,-cam-&
}}
5.计算相机变换矩阵并缓存
欧拉相机的变换矩阵在Hello3DWorld已经说的够多了,就是平移+旋转,但是位移和角度都是和物体变换相反而已。
下面介绍下UVN相机的变换矩阵。
平移是肯定要做的,这个不再多说了,说说平移之后,我们的变换矩阵是什么呢?
再看一次UVN相机的示意图:
U、V、N互相垂直,这不就是个坐标系么?
还记得上次苍井空教给咱们如何推出变换矩阵么,就是找X,Y,Z轴作为操作柄,如何变换成新的坐标柄。
设基向量&
1,0,0&
&
0,1,0&
0,0,1&
为世界坐标系三个坐标轴向量,因为相机系的相机朝向为Z正方向,所以变换对应关系为:
X轴-&
U
Y轴-&
V
Z轴-&
N
所以变换矩阵的X分量为&
Ux,Uy,Uz&
同理,Y分量&
Vx,Vy,Vz&
Z分量&
Nx,Ny,Nz&
所以UVN的变换矩阵为(不包括平移):
[UxUyUz]
[VxVyVz]
[NxNyNz]
所以可以写出下面的相机变换矩阵更新函数:
CameraUpdateMatrix(CAMERA_PTRcam)//更新相机中缓存的变换矩阵{VECTOR4Dvmove;
vmove,-cam-&
WorldPos.x,-cam-&
WorldPos.y,-cam-&
WorldPos.z);
MATRIX4X4mmove;
BuildMoveMatrix(&
vmove,&
mmove);
if(cam-&
Type==CAMERA_TYPE_ELUER){MATRIX4X4mrotation;
BuildRotateMatrix(-cam-&
Direction.x,-cam-&
Direction.y,-cam-&
Direction.z,&
mrotation);
MatrixMul(&
mmove,&
mrotation,&
MatrixCamera);
}elseif(cam-&
Type==CAMERA_TYPE_UVN){if(cam-&
UVNTargetNeedCompute){//方向角、仰角doublephi=cam-&
Direction.x;
doubletheta=cam-&
Direction.y;
doublesin_phi=FastSin(phi);
doublecos_phi=FastCos(phi);
doublesin_theta=FastSin(theta);
doublecos_theta=FastCos(theta);
UVNTarget.x=-1*sin_phi*sin_theta;
UVNTarget.y=1*cos_phi;
UVNTarget.z=1*sin_phi*cos_theta;
}//定义临时的UVN(未归一化)VECTOR4Du,v,n;
//求NVectorSub(&
UVNTarget,&
WorldPos,&
n);
//设置VVectorCopy(&
v,&
V));
//应为N和V可以组成一个平面,所以可求法向量UVectorCross(&
n,&
u);
//因为V和N可能不垂直,所以反求V,使得V和U、N都垂直VectorCross(&
u,&
v);
//UVN归一VectorNormalize(&
U);
VectorNormalize(&
V);
N);
//UVN变换矩阵MATRIX4X4muvn;
MatrixCreate(&
muvn,cam-&
U.x,cam-&
V.x,cam-&
N.x,0,cam-&
U.y,cam-&
V.y,cam-&
N.y,0,cam-&
U.z,cam-&
V.z,cam-&
N.z,0,0,0,0,1);
muvn,&
6.欧拉旋转角转UVN
有一种非常常见的用法,就是相机只知道初始的朝向,并且是欧拉旋转角表示的。
但我们还希望使用UVN系统的相机,那么就需要把欧拉相机朝向计算出UVN,这样相机就变成UVN相机了。
要做这个转换,其实就是要求得目标注视点的坐标,这里要应用球面坐标系转笛卡尔坐标系的性质来完成了。
我们先假设注视目标点在点P,我们用球面坐标系来表示这个点P(p,phi,theta):
如果您对球面坐标系还有印象的话,可以发现其实球面坐标中的phi就是欧拉旋转角中的绕Y轴旋转的角度,theta就是绕Z轴旋转的角度。
所以:
phi=cam-&
theta=cam-&
Direction.z;
我自认为这个图画的立体效果很强哈,而且重要的直角都标出来了。
因为我们只关系朝向不关系长度,所以我们设这个球面坐标的p为1。
所以可以非常容易推得:
r=sin(phi)
x=cos(theta)*r
y=sin(theta)*r
z=cos(phi)
好了,万事俱备,现在实现一个比较完善的更新缓存矩阵的函数:
UVNTargetNeedCompute){//欧拉角度求注视向量doublephi=cam-&
doubler=sin_phi;
UVNTarget.x=cos_theta*r;
UVNTarget.y=sin_theta*r;
UVNTarget.z=cos_phi;
通过type判断是缓存欧拉变换矩阵还是UVN变换矩阵,如果是UVN变换,还要判断UVNTargetNeedCompute的值,来决定是否将欧拉角度转变为注视向量,否则直接使用结构体存放的Target向量。
别的就不用多说了。
7.代码下载
这次没有多演示DEMO做什么更新,但是因为使用比较完善的相机系统,所以已经可以支持随意的操纵相机了。
完整项目代码下载:
点击进入下载页&
8.补充说明
这篇文章的知识用的比较多,我学习了好几本书,如《3D数学基础:
图形与游戏开发》。
结果发现有一本书中存不少的说法错误和问题,就是《3D游戏编程大师技巧》这本,有一些可能是翻译水平问题,还有一些光盘源码也有的问题,这里指出一下。
1)求视距,该书给出的推导公式和代码为d=(0.5)*(cam-&
viewplane_width)