Direct3D流水线大体图:
局部坐标:
也就是3D实体在其局部坐标系中的坐标,即为当创建3D物体时,或从其他地方加载3D物体时,
该物体通常都会有自己的一组坐标轴,物体的中心则位于局部坐标系的原点,如果不进行变化,
那么程序加载物体就可能会看不见物体。
如下图定义了一个立方体,该立方体长为10个单位,中心坐标为(0,0,0),各个顶点坐标分别如下:
V0(10,10,10) V1(-10,10,10)
V2(-10,10,-10) V3(10,10,-10)
V4(10,-10,10) V5(-10,-10,10)
V6(-10,-10,-10) V7(10,-10,-10)
世界坐标:
当建立好所有的3D模型后,每个模型都位于自己的局部坐标系中,需要将其放置到世界坐标系中。世界坐标表示的是各种物体在游戏世界的实际位置,是绝对位置,而非相对位置。那这个世界有多大,会不会小于物体的局部坐标,这个取决于你设计游戏地图的大小,以及物体处在这个世界中的什么地位(放大还是缩小)。
知道了世界坐标后,怎么进行将局部坐标变换为世界坐标,记得上次写过,在Direct3D中,可以通过IDirect3DDevice9::SetTransform()这个函数方法就可以加以应用,将里面的第一个参数改成自己想要变换的类型(如局部坐标到世界坐标的转换,参数变为D3DTS_WORLD),即可完成坐标之间的转换。
不过也可以自己重载一个SetTransform()函数方法。就用刚才的立方体为例。
//定义3D坐标结构体
typedef strct Point
{
int x;
int y;
int z;
}POINT3D;
//那么立方体的顶点先用一个数组来存储
POINT3D cube_model[8]={
{10,10,10},{-10,10,10}.
{-10,10,-10},{10,10,-10},
{10,-10,10},{-10,-10,10},
{-10,-10,-10},{10,-10,-10}};
当然也可以用面来存储顶点数据,如立方体共6个面,每个面共4个顶点,所以再定义面的结构体。在把顶点包含进去就OK了。
我采用的是第一种,使用POINT3D来存储立方体的各个顶点,要将局部坐标变换为世界坐标,只需知道要将物体的中心放置到游戏世界的什么位置,然后将局部坐标平移到这个位置即可,再符合游戏需求的进行放大或缩小。
POINT3D cube_world[8]; //用于存储世界坐标
//每个顶点平移(_x,_y,_z)
for(int vertex=0;vertex<8;vertex++)
{
cube_world[vertex].x=cube_model[vertex].x+_x;
cube_world[vertex].y=cube_model[vertex].y+_y;
cube_world[vertex].z=cube_model[vertex].z+_z;
}
这只是进行平移而已,可以用一个矩阵来进行表示。使用4D齐次坐标,平移矩阵表示为:
[1 0 0 0]
T= [0 1 0 0]
[0 0 1 0]
[_x _y _z 1]
既然转换矩阵为4D齐次坐标表示,那么原坐标也用4D坐标来进行表示[x y z 1];那么从局部坐标到世界坐标的转换就可以让这两个矩阵相乘。
[x y z 1]*T=[x+_x y+_y z+_z 1]
这相比刚才的for循环要简便多了。
物体的放大缩小和旋转也是一样类似。
4D齐次坐标:
齐次坐标就是将一个n维的向量用一个n+1维向量来进行表示。如2D点[x y]的其次坐标表示为[x y z];3D点[x y z]的齐次坐标表示为[x y z w];其中w是用于其次化坐标,将齐次坐标转换为常规坐标,就必须除以w。
如3D点的其次坐标:
[X Y Z W]
常规坐标:
[X/W Y/W Z/W]
如果W=1,那么X,Y,Z的值将不会改变;如果W不为1,这种变换有何用途?W可以让我们表示无穷大的概念;如当W趋于0时,X/W将趋近于无穷大。而且也方便计算机执行矩阵的运算。
相机坐标:
局部坐标在物体自己的3D空间内定义了物体,坐标系是相对于这个物体的中心即(0,0,0),而世界坐标系定义了虚拟的3D空间,物体将被放置到这个空间中。
3D游戏是围绕着3D相机进行的,因此需要采取某种方式来观察3D物体。所以才有相机坐标这个概念。
最近很火的“吃鸡”(绝地求生)就是个典型的例子。
相机坐标系是以摄像机(屏幕中显示的图形就是虚拟摄像机拍摄在胶片上的景物)为摄像机位置为原点,摄像机观察的方向向着Z轴而建立的坐标系。
不过要注意的是,该变化将摄像机变换至坐标系原点并使摄像机的方向向着Z轴正方向。空间中物体应随着摄像机一同进行变换,这样摄像机的视场才能保持不变。
在Direct3D中,提供了一些API函数使用:
//D3DXMatrixLookAtLH(),变换矩阵
D3DXMATRIX *D3DXMatrixLookAtLH(
D3DXMATRIX *pOut, //用于接收相机坐标位置的矩阵表示
CONST D3DXVECTOR3 *pEye, //相机位于世界坐标的位置
CONST D3DXVECTOR3 *pAt, //指定世界坐标系中的被观察点
CONST D3DXVECTOR3 *pUp) //世界坐标系中表示“向上”方向的向量
//如设摄像机位置位于(3,4,5)处,观察点为世界坐标的原点(0,0,0),那这样设置
D3DXVECTOR3 position(3.0f,4.0f,5.0f);
D3DXVECTOR3 targetPoint(0.0f,0.0f,0.0f);
D3DXVECTOR3 worldUp(0.0f,2.0f,0.0f);
D3DXMATRIX V;
D3DXMatrixLookAtLH(&V,&position,&targetPoint,&worldUp);
//开始变换
Device->SetTransform(D3DTS_VIEW,&V);
还是一样采用SetTransform()方法,不过要将第一个参数进行变换而已。
接下来跟着大师的步伐,学着自己重载一个。
1.视景体:
要观察3D空间中的场景,最常用的方法是将一台相机放置在世界的某个地方,然后通过特定的视景体观察世界。这个视景体,它定义了相机能够拍摄到的空间,也就是位于上面图片中的远裁剪面和近裁见面之间的范围。
视景体的平面图展示:
(1)远、近裁剪面:
这两个平面都垂直与观察方向,决定了哪些物体将被渲染到图像中。比远裁剪面更远或比近裁剪面更近的物体都不会被渲染。这也符合我们眼睛的视角。
2、世界坐标到相机坐标的变换:
两步走,先平移,再旋转。
(1)平移
这个可以采用上面局部坐标----->到世界坐标的方法,得知具体位置,采用for循环进行(加减)赋值,或者利用4D其次坐标(推荐,速度会更快)
POINT3D _camera[8]; //用于存储相机坐标
for(int vertex=0;vertex<8;vertex++)
{
_camera[vertex].x=world[vertex].x-cam_x;
_camera[vertex].y=world[vertex].y-cam_y;
_camera[vertex].z=world[vertex].z-cam_z;
}
4D其次矩阵
[1 0 0 0]
A= [0 1 0 0]
[0 0 1 0]
[-cam_x -cam_y -cam_z 1]
最后再将物体在世界的坐标位置乘上该矩阵,就可以得到了物体在相机坐标中的新位置。
新位置是有了,但得考虑下物体的朝向问题。
(2)旋转
如上所示,经过第一步平移之后,相机位于世界坐标系原点,物体相对于相机的位置保持不变,但相机的观察角仍为(0,ang_y,0)。要让相机的朝向与+Z轴重合,只需将相机绕Y轴旋转-ang_y,这是相机观察角度(0,ang_y,0)的逆。
旋转后,相机位于(0,0,0)处,观察角度为(0,0,0),此时,物体与相机之间的相对位置和角度保持不变,变的只有物体在世界空间的绝对位置发生了改变。
那怎么设置旋转呢?
相机朝向由3个角度指定(x,y,z),因此旋转顺序就会有6种:
xyz;
xzy;
yxz;
yzx;
zxy;
zyx;
无论采用哪种顺序都可以,不过具体情况具体分析,加以分析,再进行抉择,可能会达到事半功倍的效果。
应用例子(采用顺序3吧)
yxz;先绕y轴(Ry),再绕x轴(Rx),最后再绕z轴(Rz)。
A仍为上面的世界坐标到相机坐标转换的4D其次坐标表示
Tw=ARyRx*Rz
Train(练习):
//假定相机位于(cam_x,cam_y,cam_z)处
//观察角度为(ang_x,ang_y,ang_z)
//旋转顺序为yxz
//平移矩阵
MATRIX4X4 Tcam_inv={{1,0,0,0},
{0,1,0,0},
{0,0,1,0},
{-cam_x,-cam_y,-cam_z,1}};
//现在计算绕x,y和z轴旋转后的旋转矩阵
//绕x轴的逆旋转矩阵
MATRIX4X4 Rcamx_inv{{1,0,0,0},
{0,cos(-ang_x),sin(-ang_x),0},
{0,-sin(-ang_x),cos(-ang_x),0},
{0,0,0,1}};
//绕y轴的逆旋转矩阵
MATRIX4X4 Rcamy_inv={{cos(-ang_y),0,-sin(-ang_y),0},
{0,1,0,0},
{sin(-ang_y),0,cos(-ang_y),0},
{9,0,0,1}};
//绕z轴的逆旋转矩阵
MATRIX4X4 Rcamz_inv={{cos(-ang_z),sin(-ang_z),0,0},
{-sin(-ang_z),cos(-ang_z),0,0},
{0,0,1,0},
{0,0,0,1}};
//计算出所有的逆旋转矩阵,将它们相乘
//Tw=A*Ry*Rx*Rz
MATRIX4X4 Mtemp1,Mtemp2,Tcam;
//注意下顺序
Mat_Mul_4X4(&Tcam_inv,&Rcamy_inv,&Mtemp1);
Mat_Mul_4X4(&Rcamx_inv,&Rcamz_inv,&Mtemp2);
Mat_Mul_4X4(&Mtemp1,&Mtemp2,&Tcam);
//开始执行世界坐标到相机坐标变换了
for(int vertex=0;vertex<8;vertex++)
{
Mat_Mul_VECTOR3D_4X4(world[vertex],&Tcam,_camera[vertex]);
}
如果再想将其速度再变快,可以手工进行计算,把计算结果存储进去。
休息会o(▽)q,听说最近无双8快出了,画质还不错哦,战场的自由度更高
消除、消隐
像上面的图,如果不进行背面消除和消隐,那么看里面的物体的时候就会看见所有面,很别扭,而且也不符合现实生活中看物体的视觉变换,看物体的主视图居然还能看见该物体的左视图和俯视图,这可真让人摸不着头脑。
所以背面消除和消隐就很重要了。
为了实现背面消除,Direct3D需要区分哪些多边形是正面朝向的,哪些是背面朝向的。在默认状态下,Direct3D认为顶点排列顺序为顺时针(相机坐标系)三角形单元是正面朝向的,顶点排列顺序为逆时针,则三角形单元是背面朝向的。
D3D9提供一个API来进行背面消除,可以通过修改绘制状态使用D3DRS_CULLMODE来达到效果。
Device->SetRenderState(D3DRS_CULLMODE,Value);
//D3DCULL_NONE 完全禁用背面消除
//D3DCULL_CW 只针对顺时针绕序的三角形单元进行消除
//D3DCULL_CCW 只针对逆时针绕序的三角形单元进行消除
继续跟着“大师”进行重载
背面消除是测试在世界空间中进行的,测试还未执行世界坐标到相机坐标变换,用于避免对那些由于被遮住而从视点看不到的多边形进行世界坐标到相机坐标变换。
工作原理:以某种方式(顺时针或者逆时针)对构成每个物体的所有多边形进行标记,然后计算每个多边形的面法线n,并根据观察向量对这条法线进行测试。当面法线和观察向量之间的夹角<=90°,则多边形对观察者而言是可见的(不过,对于双面多边形,这个方法就不太管用了)。
采用点积来实现。
当且仅当向量u和v的夹角为90°(π/2弧度),uv=0;可见
当且仅当向量u和v的夹角<90°,uv>0;可见
当且仅当向量u和v的夹角>90°,u*v<0;不可见
现在已经解决了大多数多边形的消除问题,接下来对剩下的物体进行世界坐标到相机坐标变换,之前的背面消除是建立在世界坐标系中的;现在执行包围球测试。
包围球测试的工作原理:对于世界空间中的每个物体,创建一个将其包围起来的球体,如下图所示,然后,只对球心(单个点)执行世界坐标到相机坐标变换,并判断球体是否位于视景体内,如果不在视景体内,则丢弃它包围的整个整体。当然如果球体的一部分在视景体内,这种测试就无法得出结论,需要再做其他测试。
假设有一个物体O,它包含一组顶点,要计算包围球,可找出哪个顶点离物体中心最远,该顶点与中心之间的距离就是包围球的半径。计算出半径后,可以通过一些计算创建包围球:给定物体中心p0的世界坐标(x0,y0,z0)及其半径_radius,对中心点p0进行世界坐标到相机坐标变换:p1=p0*Tw。
Tw是上面所得出的从世界坐标到相机坐标的变换矩阵,p1是物体的包围球球心的相机坐标,接下来定义6个点,它们与p1之间的连线分别平行于±x轴,±y轴和±z轴,到p1的距离都为_radius;这些点定义了一个以p1位球心的包围球S,这样,便可以通过一些简单测试来判断包围球是否在视景体内。
光照系统
光源是在世界坐标系中定义的,但必须在相机坐标方可使用。在相机坐标系中,光源照亮场景中的物体,从而可以获得较为逼真的显示效果。
裁剪
在进行背面消除的同时,差点忘了考虑裁剪这一步骤,将位于视景体外的几何体剔除掉。
如有两个点:p1(x1,y1,z1)和p2(x2,y2,z2),它们定义了一条线段,这条线段的一部分位于视景体内。
绘制该直线时,需要使用视景体对其进行裁剪。这一步很重要,因为所有完全位于视景体内的物体都将被投影和渲染,同样所有完全位于视景体外的物体都将被剔除。然而,对于部分落在视景体内的几何体,必须对其进行裁剪,就像在2D窗口中裁剪直线和位图一样。
就对这个直线进行裁剪,处理方式有两种。
1、对直线执行投影变换(假设两个端点的z坐标都大于0),到3D流水线的光栅化阶段,再在2D空间内使用窗口对直线进行裁剪,再使用屏幕空间或视口对它们进行裁剪。
虽然易于实现,但这也极大地增加处理器的负载,(即便现在的CPU越来越强,还是能尽量减轻越好,这样就可以把占用CPU的时间腾出来,让CPU完成更为强大的工作)。
2、在物体空间(即物体或几何体所在的数学空间)中进行。在对几何体执行投影变换之前,要执行物体空间裁剪,必须在3D空间内使用视景体对所有不完全位于视景体内的几何体(直线或多边形)进行裁剪。
如图所示,其中远裁剪面为z=far_z,近裁剪面为z=near_z,投影面/视平面为d=1。
点p1~p6定义了视景体在x-z平面上的2D投影图,它们的y坐标为0:
p1(-far_z,0,far_z)
p2(far_z,0,far_z)
p3(near_z,0,near_z)
p4(1,0,1)
p5(-1,0,1)
p6(-near_z,0,near_z)
d值为1,将这些点进行投影变换,即将每个分量乘以d再除以z坐标,得到(x-z平面):
p1(-far_z/far_z,0/far_z,far_z)=(-1,0,far_z)
p2(far_z/far_z,0/far_z,far_z)=(1,0,far_z)
p3(near_z/near_z,0/near_z,near_z)=(1,0,near_z)
p4(1/1,0/1,1)=(1,0,1)
p5(-1/1,0/1,1)=(-1,0,1)
p6(-near_z/near_z,0/near_z,near_z)=(-1,0,near_z)
现在绘制出新点p1~p6,变换后为矩形,这样裁剪起来将很容易。在y-z平面进行一样的分析,得到类似的结果,将结果合并起来,将得到一个长方体视景体。
投影
现在我们的目标是将所获取的3D场景进行2D表示。
在Direct3D中,投影变换定义了视域体,并负责将视域体中的几何体投影到投影窗口上,即可完成3D场景到2D窗口的表示。
根据视域体的描述信息创建一个投影矩阵
D3DXMATRIX *D3DXMatrixPerspectiveFOVLH(
D3DXMATRIX &pOut,
FLOAT fovY,
FLOAT Aspect, //纵横比
FLOAT zn, //近裁面
FLOAT zf); //远裁面
设置近裁面到坐标原点的距离是1,远裁面到原点的距离是1000,采用如下方法进行设置:
D3DXMATRIX proj;
D3DXMatrixPerspectiveFOVLH(&proj,PI*0.5f,(float)width/(float)height,
1.0f,1000.0f);
Device->SetTransform(D3DTS_PROJECTION,&proj);
学习重载函数:
投影面:
了解透视变换:指的是将物体的顶点投影到投影面上,给定视平面与投影点(相机视点)之间的距离d,可以很容易计算出物体上任何一点和视点之间的连线与视平面的交点。
将点p(y0,z0)投影到视平面上,后者离原点的距离为d,其平面方程为z=d。利用相似三角形原理。
d/z0=yp/y0
yp=dy0/z0
同样在x-z平面上
d/z0=xp/x0
xp=dx0/z0
再将上述公式组合在一起,可以得到视平面z=d,视点位于(0,0,0)处时的投影变换:
x=dx/z;
y=dy/z;
POINT3D cube_camera[8]; //用于存储相机坐标
POINT3D cube_per[8]; //用于存储透视坐标
//对相机坐标执行透视变换
//假设视距为d
for(int vertex=0;vertex<8;vertex++)
{
float z=cube_model[vertex].z;
cube_per[vertex].x=d*cube_camera[vertex].x/z;
cube_per[vertex].y=d*cube_camera[vertex].y/z;
//不需要z坐标,复制原来的值即可
cube_per[vertex].z=cube_camera[vertex].z;
}
再次总结下,创建一个完全基于矩阵的系统,还是采用4D齐次坐标来执行投影变换
[1 0 0 0]
Tp= [0 1 0 0]
[0 0 1 1/d]
[0 0 0 0]
进行验证,使用相机坐标(xc,yc,zc,1)的点p来进行检验
pTp=[xc yc zc (zc/d)]
再将分量分别处以分量w(zc/d),将齐次坐标转换为常规坐标,结果如下
[xdd/zc, ycd/zc, zcd/zc]
不考虑坐标z,则表示为
x=xcd/zc;
y=ycd/zc;
屏幕坐标:
终于快到终点了——屏幕坐标,也就是将顶点坐标从投影窗口转换到屏幕的一个矩形区域中。
在Direct3D中,屏幕用结构D3DVIEWPORT9来表示
typedef struct D3DVIEWPORT9(
DWORD X; //相对屏幕位置
DWORD Y;
DWORD Width; //矩形区域的大小
DWORD Height;
DWORD Minz; //深度缓存的值
DWORD Maxz
}D3DVIEWPORT9;
//这样来设置屏幕窗口
D3DVIEWPORT9 vp={0,0,640,480,0,1};
Device->SetViewport(&vp);
这样就可以完成屏幕坐标的转换了。
最后一步光栅化,即可完成流水线操作。