但是我们不要忘记变换中间用到的坐标系,例如:
·世界坐标系:相对于3D世界的原点三维坐标系。
·对齐(视点)坐标系:世界坐标系的变换,观察者的位置在世界坐标系的原点。
下面是坐标的基本结构:
// 二维坐标
typedef struct {
short x, y;
} _2D;
// 三维坐标
typedef struct {
float x, y, z;
} _3D;
这里,我们定义了称为顶点的坐标结构。因为“顶点”一词指两个或两个以上菱形边的交点。我们的顶点可以简单地认为是描述不同系统的矢量。
// 不同的坐标系的坐标
typedef struct {
_3D Local;
_3D World;
_3D Aligned;
} Vertex_t;
2.2 实现矩阵系统
我们需要存储我们的矩阵在4x4浮点数矩阵中。所以当我们需要做变换是我们定义如下矩阵:
float matrix[4][4];
然后我们定义一些函数来拷贝临时矩阵到全局矩阵:
void MAT_Copy(float source[4][4], float dest[4][4])
{
int i,j;
for(i=0; i<4; i++)
for(j=0; j<4; j++)
dest[i][j]=source[i][j];
}
很简单。现在我们来写两个矩阵相乘的函数。同时可以理解上面的一些有关矩阵相乘的公式代码如下:
void MAT_Mult(float mat1[4][4], float mat2[4][4], float dest[4][4])
{
// mat1 — 矩阵1
// mat2 — 矩阵2
// dest — 相乘后的新矩阵
int i,j;
for(i=0; i<4; i++)
for(j=0; j<4; j++)
dest[i][j] = mat1[i][0]*mat2[0][j]+
mat1[i][1]*mat2[1][j]+
mat1[i][2]*mat2[2][j]+
mat1[i][3]*mat2[3][j];
}
现在你明白了吗?现在我们设计矢量与矩阵相乘的公式。
void VEC_MultMatrix(_3D *Source,float mat[4][4],_3D *Dest)
{
// Source — 源矢量(坐标)
// mat — 变换矩阵
// Dest — 目标矩阵(坐标)
Dest->x = Source->x*mat[0][0]+
Source->y*mat[1][0]+
Source->z*mat[2][0]+ mat[3][0];
Dest->y = Source->x*mat[0][1]+
Source->y*mat[1][1]+
Source->z*mat[2][1]+ mat[3][1];
Dest->z = Source->x*mat[0][2]+
Source->y*mat[1][2]+
Source->z*mat[2][2]+ mat[3][2];
}
我们已经得到了矩阵变换函数,不错吧。注意:这里的矩阵变换与我们学过的矩阵变换不同。一般的,Y = TX,T为变换矩阵,这里为Y = XT,是由于矩阵T为4x4矩阵。
2.3 实现三角法系统
几乎每一个C编译器都带有有三角函数的数学库,但是我们需要简单的三角函数时,不是每次都使用它们。正弦和余弦的计算是阶乘和除法的大量运算。为提高计算速度,我们建立自己的三角函数表。首先决定你需要的角度的个数,然后在这些地方用下面的值代替:
float SinTable[256], CosTable[256];
然后使用宏定义,它会把每一个角度变成正值,并对于大于360度的角度进行周期变换,然后返回需要的值。如果需要的角度数是2的幂次,那么我们可以使用“&”代替“%”,它使程序运行更快。例如256。所以在程序中尽量选取2的幂次。
// 三角法系统
#define SIN(x) SinTable[ABS((int)x&255)]
#define COS(x) CosTable[ABS((int)x&255)]
一旦我们已经定义了需要的东西,建立初始化函数,并且在程序中调用宏。
void M3D_Init()
{
int d;
for(d=0; d<256; d++)
{
SinTable[d]=sin(d*PI/128.0);
CosTable[d]=cos(d*PI/128.0);
}
}
2.4 建立变换矩阵
下面使用C编写的变换矩阵代码。
float mat1[4][4], mat2[4][4];
// 定义单位阵
void MAT_Identity(float mat[4][4])
{
mat[0][0]=1; mat[0][1]=0; mat[0][2]=0; mat[0][3]=0;
mat[1][0]=0; mat[1][1]=1; mat[1][2]=0; mat[1][3]=0;
mat[2][0]=0; mat[2][1]=0; mat[2][2]=1; mat[2][3]=0;
mat[3][0]=0; mat[3][1]=0; mat[3][2]=0; mat[3][3]=1;
}
// tx,ty.tz — 平移参数
// matrix — 源矩阵和目标矩阵
// 矩阵平移函数
void TR_Translate(float matrix[4][4],float tx,float ty,float tz)
{
float tmat[4][4];
tmat[0][0]=1; tmat[0][1]=0; tmat[0][2]=0; tmat[0][3]=0;
tmat[1][0]=0; tmat[1][1]=1; tmat[1][2]=0; tmat[1][3]=0;
tmat[2][0]=0; tmat[2][1]=0; tmat[2][2]=1; tmat[2][3]=0;
tmat[3][0]=tx; tmat[3][1]=ty; tmat[3][2]=tz; tmat[3][3]=1;
MAT_Mult(matrix,tmat,mat1);
MAT_Copy(mat1,matrix);
}
// 矩阵缩放
void TR_Scale(float matrix[4][4],float sx,float sy, float sz)
{
float smat[4][4];
smat[0][0]=sx; smat[0][1]=0; smat[0][2]=0; smat[0][3]=0;
smat[1][0]=0; smat[1][1]=sy; smat[1][2]=0; smat[1][3]=0;
smat[2][0]=0; smat[2][1]=0; smat[2][2]=sz;smat[2][3]=0;
smat[3][0]=0; smat[3][1]=0; smat[3][2]=0; smat[3][3]=1;
MAT_Mult(matrix,smat,mat1);
MAT_Copy(mat1,matrix);
}
// ax — 绕X轴旋转的角度
// ay — 绕Y轴旋转的角度
// az — 绕Z轴旋转的角度
// 矩阵旋转
void TR_Rotate(float matrix[4][4],int ax,int ay,int az)
{
float xmat[4][4], ymat[4][4], zmat[4][4];
xmat[0][0]=1; xmat[0][1]=0; xmat[0][2]=0; xmat[0][3]=0;
xmat[1][0]=0; xmat[1][1]=COS(ax); xmat[1][2]=SIN(ax); xmat[1][3]=0;
xmat[2][0]=0; xmat[2][1]=-SIN(ax); xmat[2][2]=COS(ax); xmat[2][3]=0;
xmat[3][0]=0; xmat[3][1]=0; xmat[3][2]=0; xmat[3][3]=1;
ymat[0][0]=COS(ay); ymat[0][1]=0; ymat[0][2]=-SIN(ay); ymat[0][3]=0;
ymat[1][0]=0; ymat[1][1]=1; ymat[1][2]=0; ymat[1][3]=0;
ymat[2][0]=SIN(ay); ymat[2][1]=0; ymat[2][2]=COS(ay); ymat[2][3]=0;
ymat[3][0]=0; ymat[3][1]=0; ymat[3][2]=0; ymat[3][3]=1;
zmat[0][0]=COS(az); zmat[0][1]=SIN(az); zmat[0][2]=0; zmat[0][3]=0;
zmat[1][0]=-SIN(az); zmat[1][1]=COS(az); zmat[1][2]=0; zmat[1][3]=0;
zmat[2][0]=0; zmat[2][1]=0; zmat[2][2]=1; zmat[2][3]=0;
zmat[3][0]=0; zmat[3][1]=0; zmat[3][2]=0; zmat[3][3]=1;
MAT_Mult(matrix,ymat,mat1);
MAT_Mult(mat1,xmat,mat2);
MAT_Mult(mat2,zmat,matrix);
}
2.5 如何建立透视
如何建立对象的立体视觉,即显示器上的一些事物看起来离我们很近,而另外一些事物离我们很远。透视问题一直是困绕我们的一个问题。有许多方法被使用。我们使用的3D世界到2D屏幕的投影公式:
P( f ):(x, y, z)==>( f*x / z + XOrigin, f*y / z + YOrigin )
其中f是“焦点距离”,它表示从观察者到屏幕的距离,一般在80到200厘米之间。XOrigin和YOrigin是屏幕中心的坐标,(x,y,z)在对齐坐标系上。那么投影函数应该是什么样?
// 定义焦点距离
#define FOCAL_DISTANCE 200
// 得到屏幕上的投影坐标。因为0不能做除数,所以对z进行判断。
void Project(vertex_t * Vertex)
{
if(!Vertex->Aligned.z)
Vertex->Aligned.z = 1;
Vertex->Screen.x = FOCAL_DISTANCE * Vertex->Aligned.x / Vertex->Aligned.z + XOrigin;
Vertex->Screen.y = FOCAL_DISTANCE * Vertex->Aligned.y / Vertex->Aligned.z + YOrigin;
}
2.6 变换对象
既然我们已经掌握了所有的变换顶点的工具,就应该了解需要执行的主要步骤。
1)初始化每一个顶点的本地坐标。
2)设置全局矩阵为单位阵。
3)根据对象的尺寸缩放全局矩阵。
4)根据对象的角度来旋转全局矩阵。
5)根据对象的位置移动全局矩阵。
6)把本地坐标乘以全局矩阵来得到世界坐标系。
7)设置全局矩阵为单位阵。
8)用观测者的位置的负值平移全局矩阵。
9)用观测者的角度的负值旋转全局矩阵。
10)把世界坐标系与全局矩阵相乘得到对齐坐标系。
11)投影对齐坐标系来得到屏幕坐标。
即:本地坐标系 —> 世界坐标系 —> 对齐坐标系 —> 屏幕坐标系
回目录
--------------------------------------------------------------------------------
3、多边形填充
3.1 多边形结构
我们如何存储我们的多边形?首先,我们必须知道再这种状态下多边形是二维多边形,而且由于初始多边形是三维的,我们仅需要一个临时的二维多边形,所以我们能够设置二维顶点的最大数为一个常量,而没有浪费内存:
2D结构:
typedef struct {
_2D Points[20];
int PointsCount;
int Texture;
} Polygon2D_t;
3D结构:
typedef struct {
int Count;
int * Vertex;
int Texture;
Vertex_t P,M,N;
} Polygon_t;
为什么顶点数组包含整数值呢?仔细思考一下,例如在立方体内,三个多边形共用同一个顶点,所以在三个多边形里存储和变换同一个顶点会浪费内存和时间。我们更愿意存储它们在一个对象结构里,而且在多边形结构里,我们会放置相应顶点的索引。请看下面的结构:
typedef struct {
int VertexCount;
int PolygonCount;
Vertex_t * Vertex;
Polygon_t * Polygon;
_3D Scaling;
_3D Position;
_3D Angle;
int NeedUpdate;
} Object_t;
3.2 发现三角形
因为绘制一个三角形比绘制任意的多边形要简单,所以我们从把多边形分割成三顶点的形状。这种方法非常简单和直接:
void POLY_Draw(Polygon2D_t *Polygon)
{
_2D P1,P2,P3;
int i;
P1 = Polygon->Points[0];
for(i=1; i < Polygon->PointsCount-1; i++)
{
P2=Polygon->Points[i];
P3=Polygon->Points[i+1];
POLY_Triangle(P1,P2,P3,Polygon->Texture);
}
}
上面的算法,对于凹多边形就不太适用。
3.3 绘制三角形
现在怎样得到三角形函数?我们怎样才能画出每一条有关的直线,并且如何发现每一行的起始和结实的x坐标。我们通过定义两个简单有用的宏定义开始来区别垂直地两个点和两个数:
#define MIN(a,b) ((a<b)?(a):(b))
#define MAX(a,b) ((a>b)?(a):(b))
#define MaxPoint(a,b) ((a.y > b.y) ? a : b)
#define MinPoint(a,b) ((b.y > a.y) ? a : b)
然后我们定义三个宏来区别三个点:
#define MaxPoint3(a,b,c) MaxPoint(MaxPoint(a,b),MaxPoint(b,c))
#define MidPoint3(a,b,c) MaxPoint(MinPoint(a,b),MinPoint(a,c))
#define MinPoint3(a,b,c) MinPoint(MinPoint(a,b),MinPoint(b,c))
你也许注意到MidPoint3宏不总是正常地工作,取决于三个点排列的顺序。例如,a<b & a<c。那么MidPoint3得到的是a,但它不是中间点。我们用if语句来修正这个缺点,下面为函数的代码:
void POLY_Triangle(_2D p1,_2D p2,_2D p3,char c)
{
_2D p1d,p2d,p3d;
int xd1,yd1,xd2,yd2,i;
int Lx,Rx;
首先我们把三个点进行排序:
p1d = MinPoint3(p1,p2,p3);
p2d = MidPoint3(p2,p3,p1);
p3d = MaxPoint3(p3,p1,p2);
当调用这些宏的时候为什么会有点的顺序的改变?(作者也不清楚)可能这些点被逆时针传递。试图改变这些宏你的屏幕显示的是垃圾。现在我们并不确定中间的点,所以我们做一些检查,而且在这种状态下,得到的中间点有似乎是错误的,所以我们修正:
if(p2.y < p1.y)
{
p1d=MinPoint3(p2,p1,p3);
p2d=MidPoint3(p1,p3,p2);
}
这些点的排列顺序看起来很奇怪,但是试图改变他们那么所有的东西就乱套了。只有理解或接受这些结论。现在我们计算增量:
xd1=p2d.x-p1d.x;
yd1=p2d.y-p1d.y;
xd2=p3d.x-p1d.x;
yd2=p3d.y-p1d.y;
好的,第一步已经完成,如果有增量y:
if(yd1)
for(i=p1d.y; i<=p2d.y; i++)
{
// 我们用x的起始坐标计算x值,在当前点和起始点之间加上增量y,乘以斜率(x/y)的相反值。
Lx = p1d.x + ((i - p1d.y) * xd1) / yd1;
Rx = p1d.x + ((i - p1d.y) * xd2) / yd2;
// 如果不在同一个点,绘制线段,按次序传递这两个点:
if(Lx!=Rx)
VID_HLine(MIN(Lx,Rx),MAX(Lx,Rx),i,c);
}
现在我们重新计算第一个增量,而且计算第二条边:
xd1=p3d.x-p2d.x;
yd1=p3d.y-p2d.y;
if(yd1)
for(i = p2d.y; i <= p3d.y; i++)
{
Lx = p1d.x + ((i - p1d.y) * xd2) / yd2;
Rx = p2d.x + ((i - p2d.y) * xd1) / yd1;
if(Lx!=Rx)
VID_HLine(MIN(Lx,Rx),MAX(Lx,Rx),i,c);
}
}
以上我们已经得到多边形填充公式,对于平面填充更加简单:
void VID_HLine(int x1, int x2, int y, char c)
{
int x;
for(x=x1; x<=x2; x++)
putpixel(x, y, c);
}
回目录
--------------------------------------------------------------------------------
4、Sutherland-Hodgman剪贴
4.1 概述
一般地,我们更愿意剪贴我们的多边形。必须靠着屏幕的边缘剪贴,但也必须在观察的前方(我们不需要绘制观察者后面的事物,当z左边非常小时)。当我们剪贴一个多边形,并不考虑是否每一个点在限制以内,而我们更愿意增加必须的顶点,所以我们需要一个第三个多边形结构:
typedef struct {
int Count;
_3D Vertex[20];
} CPolygon_t;
由于我们有附加的顶点来投影,我们不再投影顶点,而是投影剪贴的3D多边形到2D多边形。
void M3D_Project(CPolygon_t *Polygon,Polygon2D_t *Clipped,int focaldistance)
5.2 背面消除
除了这些方法,我们可以很容易地消除多边形的背面来节省大量的计算时间。首先,我们定义一些有用的函数来计算平面和法向量以及填充。然后,我们给这个函数增加纹理和阴影计算。这些变量为全局变量:
float A,B,C,D;
BOOL backface;
下面是我们的引擎函数,每一个坐标都是浮点变量:
void ENG3D_SetPlane(Polygon_t *Polygon,Object_t *Object)
{
float x1=Vert(0).Aligned.x;
float x2=Vert(1).Aligned.x;
float x3=Vert(2).Aligned.x;
float y1=Vert(0).Aligned.y;
float y2=Vert(1).Aligned.y;
float y3=Vert(2).Aligned.y;
float z1=Vert(0).Aligned.z;
float z2=Vert(1).Aligned.z;
float z3=Vert(2).Aligned.z;
然后我们计算平面等式的每一个成员:
A=y1*(z2-z3)+y2*(z3-z1)+y3*(z1-z2);
B=z1*(x2-x3)+z2*(x3-x1)+z3*(x1-x2);
C=x1*(y2-y3)+x2*(y3-y1)+x3*(y1-y2);
D=-x1*(y2*z3-y3*z2)-x2*(y3*z1-y1*z3)-x3*(y1*z2-y2*z1);
再检查是否它面朝我们或背朝:
backface=D<0;
}
5.3 Z-缓存
Z-缓存是把显示在屏幕上的每一个点的z坐标保持在一个巨大的数组中,并且当我们我们检查是否它靠近观察者或是否在观察者后面。我们仅仅在第一种情况下绘制它。所以我们不得不计算每一个点的z值。但是首先,我们定义全局树组和为他分配空间。(内存等于追至方向与水平方向的乘积):
typedef long ZBUFTYPE;
ZBUFTYPE *zbuffer;
zbuffer=(ZBUFTYPE *)malloc(sizeof(ZBUFTYPE)*MEMORYSIZE);
我们使用长整形作为z-缓存类型,因为我们要使用定点数。我们必须记住设置每一个z坐标来尽可能得到更快的速度:
int c;
for(c=0; c< MEMORYSIZE; c++)
zbuffer[c]=-32767;
下面是数学公式。如何才能发现z坐标?我们仅仅已经定义的顶点,而不是多边形的每一个点。实际上,我们所需要做的是投影的反变换,投影公式是:
u = f ?x / z
和
v = f ?y / z
其中u是屏幕上x的坐标,最小值为XOrigin,v是屏幕上的y的坐标,最小值YOrigin。平面公式是:
D = 0
一旦我们已经得到分离的x和y,有:
x = uz / f
和
y = vz / f
如果我们在平面等式中替代变量,公式变为:
A(uz / f) + B(vz / f) + Cz = -D
我们可以提取z分量:
z(A(u / f) + B(v / f) + C) = -D
所以我们得到z:
z = -D / (A(u / f) + B(v / f) + C)
但是由于对于每一个像素我们需要执行以上的除法,而计算1/z将提高程序的速度:
1 / z = -(A(u / f) + B(v / f) +C) / D
1 / z = -(A / (fD))u - (B / (fD))v - C / D
所以在一次像素程序运行的开始:
1 / z = -(A / (fD))u1 - (B / (fD))v - C / D
对于每一个像素,增量为:
-(A / (fD))>
下面是程序:
#define FIXMUL (1<<20)
int offset=y*MODEINFO.XResolution+x1;
int i=x1-Origin.x, j=y-Origin.y;
float z_,dz_;
ZBUFTYPE z,dz;
// 初始化 1/z 值 (z: 1/z)
dz_=((A/(float)Focal_Distance)/-D);
z_=((dz_*i)+( (B*j/(float)Focal_Distance) + C) /-D);
dz=dz_*FIXMUL;
z=z_*FIXMUL;
然后,对于每一个像素,我们简单的计算:
if(z>ZBuffer[offset])
{
zbuffer[offset]=z;
SCREENBUFFER[offset]=color;
}
z+=dz;
回目录
--------------------------------------------------------------------------------
6 6、三维纹理映射
6.1 概述
在做纹理映射时首先考虑的是建立纹理数组和初始化3D纹理坐标。纹理将存储在:
#define MAXTEXTURES 16
bitmap_t Textures[MAXTEXTURES];
我们从PCX文件分配和加载纹理。这里假设纹理大小为64x64。我们使用polygon_t结构的纹理坐标:
vertex_t P,M,N;
我们在函数中初始化纹理,该函数在建立多边形后被调用。P是纹理的原点,M是纹理的水平线末端,N是垂直线的末端。
void TEX_Setup(Polygon_t * Polygon, Object_t *Object)
{
Polygon->P.Local=P3D(Vert(1).Local.x, Vert(1).Local.y, Vert(1).Local.z);
Polygon->M.Local=P3D(Vert(0).Local.x, Vert(0).Local.y, Vert(0).Local.z);
Polygon->N.Local=P3D(Vert(2).Local.x, Vert(2).Local.y, Vert(2).Local.z);
}
我们需要象任何其他对象的顶点一样变换纹理坐标,所以我们需要建立世界变换和一个对齐变换函数:
void TR_Object(Object_t *Object, float matrix[4][4])
{
int v,p;
for(v=0; vVertexCount; v++)
VEC_MultMatrix(&Object->Vertex[v].Local,matrix,&Object->Vertex[v].World);
for(p=0; pPolygonCount; p++)
{
VEC_MultMatrix(&Object->Polygon[p].P.Local,matrix,&Object->Polygon[p].P.World);
VEC_MultMatrix(&Object->Polygon[p].M.Local,matrix,&Object->Polygon[p].M.World);
VEC_MultMatrix(&Object->Polygon[p].N.Local,matrix,&Object->Polygon[p].N.World);
}
}
void TR_AlignObject(Object_t *Object, float matrix[4][4])
{
int v,p;
for(v=0; vVertexCount; v+%2