3D圖象算法(轉)



原文連接c++

3D簡介
   咱們首先從座標系統開始。你也許知道在2D裏咱們常用Ren?笛卡兒座標系統在平面上來識別點。咱們使用二維(X,Y):X表示水平軸座標,Y表示縱軸座標。在3維座標系,咱們增長了Z,通常用它來表示深度。因此爲表示三維座標系的一個點,咱們用三個參數(X,Y,Z)。這裏有不一樣的笛卡兒三維繫統可使用。可是它們都是左手螺旋或右手螺旋的。右手螺旋是右手手指的捲曲方向指向Z軸正方向,而大拇指指向X軸正方向。左手螺旋是左手手指的捲曲方向指向Z軸負
方向。實際上,咱們能夠在任何方向上旋轉這些座標系,並且它們仍然保持自己的特性。在計算機圖形學,經常使用座標系爲左手座標系,因此咱們也使用它。:

X 正軸朝右
Y 正軸向上
Z 正軸指向屏幕裏

矢量
什麼是矢量?幾句話,它是座標集合。首先咱們從二維矢量開始,(X,Y):例如矢量P(4,5)(通常,咱們用->表示矢量)。咱們認爲矢量P表明點(4,5),它是從原點指向(4,5)的有方向和長度的箭頭。咱們談論矢量的長度指從原點到該點的距離。二維距離計算公式是
| P | = sqrt( x^2 + y^2 )
這裏有一個有趣的事實:在1D(點在單一的座標軸上),平方根爲它的絕對值。讓咱們討論三維矢量:例如P(4, -5, 9),它的長度爲
| P | = sqrt( x^2 + y^2 + z^2 )
它表明在笛卡兒3D空間的一個點。或從原點到該點的一個箭頭表明該矢量。在有關操做一節裏,咱們討論更多的知識。

矩陣
開始,咱們從簡單的開始:咱們使用二維矩陣4乘4矩陣,爲何是4乘4?由於咱們在三維座標系裏並且咱們須要附加的行和列來完成計算工做。在二維座標系咱們須要3乘3矩陣。着意味着咱們在3D中有4個水平參數和4個垂直參數,一共16個。例如:
4x4單位矩陣
| 1 0 0 0 |
| 0 1 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
由於任何其它矩陣與之相乘都不改變,因此稱之爲單位陣。又例若有矩陣以下:
| 10 -7 22 45 |
| sin(a) cos(a) 34 32 |
| -35 28 17 6 |
| 45 -99 32 16 |

有關矢量和矩陣的操做
咱們已經介紹了一些很是簡單的基本概念,那麼上面的知識與三維圖形有什麼關係呢?
本節咱們介紹3D變換的基本知識和其它的一些概念。它仍然是數學知識。咱們要討論
有關矢量和矩陣操做。讓咱們從兩個矢量和開始:

( x1 , y1 , z1 ) + ( x2 , y2 , z2 ) = ( x1 + x2 , y1 + y2 , z1 + z2 )

很簡單,如今把矢量乘於係數:

k ?( x, y, z ) = ( kx, ky, kz )
能夠把上面的公式稱爲點積,以下表示:
(x1 , y1 , z1 ) ?( x2 , y2 , z2 ) = x1x2 + y1y2 + z1z2
實際上,兩個矢量的點積被它們的模的乘積除,等於兩個矢量夾角的餘弦。因此
cos (V ^ W) =V ?W / | V | | W |

注意"^"並不表示指數而是兩個矢量的夾角。點積能夠用來計算光線於平面的夾角,咱們在計算陰影一節裏會詳細討論。
如今討論叉乘:

( x1 , y1 , z1 ) X ( x2 , y2 , z2 ) =
( y1z2 - z1y2 , z1x2 - x1z2 , x1y2 - y1x2 )

叉乘對於計算屏幕的法向量很是有用。

OK,咱們已經講完了矢量的基本概念。咱們開始兩個矩陣的和。它與矢量相加很是類似,這裏就不討論了。設I是矩陣的一行,J是矩陣的一列,(i,j)是矩陣的一個元素。咱們討論與3D變換有關的重要的矩陣操做原理。兩個矩陣相乘,並且M x N <> N x M。例如:
A 4x4矩陣相乘公式
若是 A=(aij)4x4, B=(bij)4x4, 那麼
A x B=
| S> a1jbj1 S> a1jbj2 S> a1jbj3 S> a1jbj4 |
| |
| S> a2jbj1 S> a2jbj2 S> a2jbj3 S> a2jbj4 |
| |
| S> a3jbj1 S> a3jbj2 S> a3jbj3 S> a3jbj4 |
| |
| S> a4jbj1 S> a4jbj2 S> a4jbj3 S> a4jbj4 |

其中 j=1,2,3,4

並且若是 AxB=(cik)4x4 那麼咱們能夠在一行上寫下:
cik = S>4, j=1 aijbjk
( a1, a2, a3 ) x B =
(Sum(aibi1) + b4,1, Sum(aibi2) + b4,2, Sum(aibi3) + b4,3 )

如今,咱們能夠試着把一些矩陣乘以單位陣來了解矩陣相乘的性質。咱們把矩陣與矢量相乘結合在一塊兒。下面有一個公式把3D矢量乘以一個4x4矩陣(獲得另一個三維矢量)若是B=(bij)4x4,那麼:
( a1, a2, a3 ) x B = (S>aibi1 + b4,1, S>aibi2 + b4,2, S>aibi3 + b4,3 )>

這就是矢量和矩陣操做公式。從這裏開始,代碼與數學之間的聯繫開始清晰。

變換
咱們已經見過象這樣的公式:
t( tx, ty ): ( x, y ) ==> ( x + tx, y + ty )

這是在二維笛卡兒座標系的平移等式。下面是縮放公式:

s( k ): ( x, y ) ==> ( kx, ky )

旋轉等式:
r( q ): ( x, y ) ==> ( x cos(q) - y sin(q), x sin(q) + y cos(q) )
以上都是二維公式,在三維裏公式的形式仍然很相近。
平移公式:
t( tx, ty, tz ): ( x, y, z ) ==> ( x + tx, y + ty, z + tz )
縮放公式:
s( k ): ( x, y, z ) ==> ( kx, ky, kz )
旋轉公式(圍繞Z軸):
r( q ): ( x, y, z ) ==> ( x cos(q) - y sin(q), x sin(q) + y cos(q), z )
因此咱們能夠寫出像在二維中一樣的變換公式。咱們經過乘以變換矩陣而獲得新的矢量,新矢量將指向變換點。下面是全部三維變換矩陣:

平移(tx, ty, tz)的矩陣

| 1 0 0 0 |
| 0 1 0 0 |
| 0 0 1 0 |
| tx ty tz 1 |


縮放(sx, sy, sz)的矩陣
| sz 0 0 0 |
| 0 sy 0 0 |
| 0 0 sx 0 |
| 0 0 0 1 |


繞X軸旋轉角q的矩陣
| 1 0 0 0 |
| 0 cos(q) sin(q) 0 |
| 0 -sin(q) cos(q) 0 |
| 0 0 0 1 |



繞Y軸旋轉角q的矩陣:
| cos(q) 0 -sin(q) 0 |
| 0 1 0 0 |
| sin(q) 0 cos(q) 0 |
| 0 0 0 1 |

繞Z軸旋轉角q的矩陣:
| cos(q) sin(q) 0 0 |
|-sin(q) cos(q) 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |

因此咱們已經能夠結束關於變換的部分.經過這些矩陣咱們能夠對三維點進行任何變換.

平面和法向量
平面是平坦的,無限的,指向特定方向的表面能夠定義平面以下:
Ax + By + Cz + D = 0

其中 A, B, C稱爲平面的法向量,D是平面到原點的距離。咱們能夠經過計算平面上的兩個矢量的叉積獲得平面的法向量。爲獲得這兩個矢量,咱們須要三個點。P1,P2,P3逆時針排列,能夠獲得:
矢量1 = P1 - P2



矢量2 = P3 - P2


計算法向量爲:
法向量 = 矢量1 X 矢量2

把D移到等式的右邊獲得:
D = - (Ax + By + Cz)



D = - (A??1.x + B??2.y + C??3.z)>

或更簡單:

D = - Normal ?P1>

可是爲計算A,B,C份量。能夠簡化操做按以下等式:
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 ( y2z3 - y3z2 ) - x2 ( y3z1 - y1z3 ) - x3 ( y1z2 - y2z1 )

三維變換
存儲座標
實現矩陣系統
實現三角法系統
建立變換矩陣
如何建立透視
變換對象

存儲座標
首先能夠編寫星空模擬代碼。那麼咱們基本的結構是什麼樣?每個對象的描述是如何存儲的?爲解決這個問題,首先咱們思考另外一個問題:咱們須要的是什麼樣的座標系?最明顯的答案是:
屏幕座標系:相對於顯示器的原點的2D座標系
本地座標系:相對於對象的原點的3D座標系
可是咱們不要忘記變換中間用到的座標系,例如:
世界座標系:相對於3D世界的原點三維座標系
對齊(視點)座標系:世界座標系的變換,觀察者的位置在世界座標系的原點。

下面是座標的基本結構:

// 二維座標
typedef struct
{
short x, y;
}_2D;

//三維座標
typedef struct
{
float x, y, z;
}_3D;

這裏,咱們定義了稱爲頂點的座標結構。由於「頂點」一詞指兩個或兩個以上菱形邊的
交點。咱們的頂點能夠簡單地認爲是描述不一樣系統的矢量。

//不一樣的座標系的座標
typedef struct
{
_3D Local;
_3D World;
_3D Aligned;
}Vertex_t;

實現矩陣系統
咱們須要存儲咱們的矩陣在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])
{
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];
}
//mat1----矩陣1
//mat2----矩陣2
//dest----相乘後的新矩陣

如今你明白了嗎?如今咱們設計矢量與矩陣相乘的公式。
void VEC_MultMatrix(_3D *Source,float mat[4][4],_3D *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];
}
//Source-----源矢量(座標)
//mat--------變換矩陣
//Dest-------目標矩陣(座標)


咱們已經獲得了矩陣變換函數,不錯吧!!
//注意,這裏的矩陣變換與咱們學過的矩陣變換不一樣
//通常的,Y=TX,T爲變換矩陣,這裏爲Y = XT,
//因爲矩陣T爲4x4矩陣

實現三角法系統
幾乎每個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);
}
}

創建變換矩陣
下面使用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;
}
//定義單位陣

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);
}
//tx,ty.tz------平移參數
//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);
}
//矩陣縮放

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);
}
//ax------繞X軸旋轉的角度
//ay------繞Y軸旋轉的角度
//az------繞Z軸旋轉的角度
//矩陣旋轉

如何創建透視
如何創建對象的立體視覺,即顯示器上的一些事物看起來離咱們很近,而另一些事物離咱們很遠。透視問題一直是困繞咱們的一個問題。有許多方法被使用。咱們使用的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
//定義焦點距離
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;
}
//獲得屏幕上的投影座標
由於0不能作除數,因此對z進行判斷。


變換對象
既然咱們已經掌握了全部的變換頂點的工具,就應該瞭解須要執行的主要步驟。
1、初始化每個頂點的本地座標
2、設置全局矩陣爲單位陣
3、根據對象的尺寸縮放全局矩陣
4、根據對象的角度來旋轉全局矩陣
5、根據對象的位置移動全局矩陣
6、把本地座標乘以全局矩陣來獲得世界座標系
7、設置全局矩陣爲單位陣
8、用觀測者的位置的負值平移全局矩陣
9、用觀測者的角度的負值旋轉全局矩陣
10、把世界座標系與全局矩陣相乘獲得對齊座標系
11、投影對齊座標系來獲得屏幕座標
即:本地座標系-->世界座標系-->對齊座標系-->屏幕座標系


多邊形填充
多邊形結構
發現三角形
繪製三角形

多邊形結構
咱們如何存儲咱們的多邊形?首先,咱們必須知道再這種狀態下多邊形是二維多邊形,並且因爲初始多邊形是三維的,咱們僅須要一個臨時的二維多邊形,因此咱們可以設置二維頂點的最大數爲一個常量,而沒有浪費內存:

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;

發現三角形
由於繪製一個三角形比繪製任意的多邊形要簡單,因此咱們從把多邊形分割成
三頂點的形狀。這種方法很是簡單和直接:
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);
}
}
//上面的算法,對於凹多邊形就不太適用
_____
|\ |
| \ |
|____\|

繪製三角形
如今怎樣獲得三角形函數?咱們怎樣才能畫出每一條有關的直線,而且如何發現
每一行的起始和結實的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;

OK,第一步已經完成,若是有增量 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);
}

Sutherland-Hodgman剪貼
概述
Z-剪貼
屏幕剪貼

概述
通常地,咱們更願意剪貼咱們的多邊形。必須靠着屏幕的邊緣剪貼,但也必須在觀察的前方(咱們不須要繪製觀察者後面的事物,當z左邊很是小時)。當咱們剪貼一個多邊形,並不考慮是否每個點在限制之內,而咱們更願意增長必須的頂點,因此咱們須要一個第三個多邊形結構:
typedef struct
{
int Count;
_3D Vertex[20];
}CPolygon_t;

因爲咱們有附加的頂點來投影,咱們再也不投影頂點,而是投影剪貼的3D多邊形到
2D多邊形。
void M3D_Project(CPolygon_t *Polygon,Polygon2D_t *Clipped,int focaldistance)
{
int v;
for(v=0; v<Polygon->Count; v++)
{
if(!Polygon->Vertex[v].z)Polygon->Vertex[v].z++;
Clipped->Points[v].x=Polygon->Vertex[v].x*focaldistance/Polygon->Vertex[v].z+Origin.x;
Clipped->Points[v].y=Polygon->Vertex[v].y*focaldistance/Polygon->Vertex[v].z+Origin.y;
}
Clipped->PointsCount=Polygon->Count;
}

Z-剪貼
首先咱們定義計算在第一個點和第二個點之間以及在第一個點和最小z值的z增量的宏。
而後,咱們計算比例,注意不要被零除。
WORD ZMin=20;
#define INIT_ZDELTAS dold=V2.z-V1.z; dnew=ZMin-V1.z;
#define INIT_ZCLIP INIT_ZDELTAS if(dold) m=dnew/dold;

咱們創建一個函數,它主要剪貼多邊形指針的參數(它將記下做爲結果的剪貼的頂點),第一個頂點(咱們剪貼的邊的開始)和第二個頂點(最後):
void CLIP_Front(CPolygon_t *Polygon,_3D V1,_3D V2)
{
float dold,dnew, m=1;
INIT_ZCLIP

如今咱們必須檢測邊緣是否徹底地在視口裏,離開或進入視口。若是邊緣沒有徹底地
在視口裏,咱們計算視口與邊緣的交線,用m值表示,用INIT_ZCLIP計算。

若是邊緣在視口裏:
if ( (V1.z>=ZMin) && (V2.z>=ZMin) )
Polygon->Vertex[Polygon->Count++]=V2;

若是邊緣正離開視口:
if ( (V1.z>=ZMin) && (V2.z<ZMin) )
{
Polygon->Vertex[Polygon->Count ].x=V1.x + (V2.x-V1.x)*m;
Polygon->Vertex[Polygon->Count ].y=V1.y + (V2.y-V1.y)*m;
Polygon->Vertex[Polygon->Count++ ].z=ZMin;
}

若是邊緣正進入視口:
if ( (V1.z<ZMin) && (V2.z>=ZMin) )
{
Polygon->Vertex[Polygon->Count ].x=V1.x + (V2.x-V1.x)*m;
Polygon->Vertex[Polygon->Count ].y=V1.y + (V2.y-V1.y)*m;
Polygon->Vertex[Polygon->Count++ ].z=ZMin;
Polygon->Vertex[Polygon->Count++ ]=V2;
}
這就是邊緣Z-剪貼函數
}

如今咱們能夠寫下完整的多邊形Z-剪貼程序。爲了有表明性,定義一個宏用來
在一個對象結構中尋找適當的多邊形頂點。
#define Vert(x) Object->Vertex[Polygon->Vertex[x]]

下面是它的函數:
void CLIP_Z(Polygon_t *Polygon,Object_t *Object,CPolygon_t *ZClipped)
{
int d,v;
ZClipped->Count=0;
for (v=0; v<Polygon->Count; v++)
{
d=v+1;
if(d==Polygon->Count)d=0;
CLIP_Front(ZClipped, Vert(v).Aligned,Vert(d).Aligned);
}
}

這個函數至關簡單:它僅僅調用FrontClip函數來作頂點交換。

剪貼屏幕
剪貼屏幕的邊緣同Z-剪貼相同,可是咱們有四個邊緣而不是一個。因此咱們須要四個
不一樣的函數。可是它們須要一樣的增量初始化:
#define INIT_DELTAS dx=V2.x-V1.x; dy=V2.y-V1.y;
#define INIT_CLIP INIT_DELTAS if(dx)m=dy/dx;

邊緣是:
_2D TopLeft, DownRight;

爲了進一步簡化_2D和 _3D結構的使用,咱們定義兩個有用的函數:
_2D P2D(short x, short y)
{
_2D Temp;
Temp.x=x;
Temp.y=y;
return Temp;
}
_3D P3D(float x,float y,float z)
{
_3D Temp;
Temp.x=x;
Temp.y=y;
Temp.z=z;
return Temp;
}

而後使用這兩個函數來指定視口:
TopLeft=P2D(0, 0);
DownRight=P2D(319, 199);

下面是四個邊緣剪貼函數:
/*
=======================
剪貼左邊緣
=======================
*/
void CLIP_Left(Polygon2D_t *Polygon,_2D V1,_2D V2)
{
float dx,dy, m=1;
INIT_CLIP

// ************OK************
if ( (V1.x>=TopLeft.x) && (V2.x>=TopLeft.x) )
Polygon->Points[Polygon->PointsCount++]=V2;
// *********LEAVING**********
if ( (V1.x>=TopLeft.x) && (V2.x<TopLeft.x) )
{
Polygon->Points[Polygon->PointsCount].x=TopLeft.x;
Polygon->Points[Polygon->PointsCount++].y=V1.y+m*(TopLeft.x-V1.x);
}
// ********ENTERING*********
if ( (V1.x<TopLeft.x) && (V2.x>=TopLeft.x) )
{
Polygon->Points[Polygon->PointsCount].x=TopLeft.x;
Polygon->Points[Polygon->PointsCount++].y=V1.y+m*(TopLeft.x-V1.x);
Polygon->Points[Polygon->PointsCount++]=V2;
}
}
/*
=======================
剪貼右邊緣
=======================
*/

void CLIP_Right(Polygon2D_t *Polygon,_2D V1,_2D V2)
{
float dx,dy, m=1;
INIT_CLIP
// ************OK************
if ( (V1.x<=DownRight.x) && (V2.x<=DownRight.x) )
Polygon->Points[Polygon->PointsCount++]=V2;
// *********LEAVING**********
if ( (V1.x<=DownRight.x) && (V2.x>DownRight.x) )
{
Polygon->Points[Polygon->PointsCount].x=DownRight.x;
Polygon->Points[Polygon->PointsCount++].y=V1.y+m*(DownRight.x-V1.x);
}
// ********ENTERING*********
if ( (V1.x>DownRight.x) && (V2.x<=DownRight.x) )
{
Polygon->Points[Polygon->PointsCount].x=DownRight.x;
Polygon->Points[Polygon->PointsCount++].y=V1.y+m*(DownRight.x-V1.x);
Polygon->Points[Polygon->PointsCount++]=V2;
}
}
/*
=======================
剪貼上邊緣
=======================
*/
void CLIP_Top(Polygon2D_t *Polygon,_2D V1,_2D V2)
{
float dx,dy, m=1;
INIT_CLIP
// ************OK************
if ( (V1.y>=TopLeft.y) && (V2.y>=TopLeft.y) )
Polygon->Points[Polygon->PointsCount++]=V2;
// *********LEAVING**********
if ( (V1.y>=TopLeft.y) && (V2.y<TopLeft.y) )
{
if(dx)
Polygon->Points[Polygon->PointsCount].x=V1.x+(TopLeft.y-V1.y)/m;
else
Polygon->Points[Polygon->PointsCount].x=V1.x;
Polygon->Points[Polygon->PointsCount++].y=TopLeft.y;
}
// ********ENTERING*********
if ( (V1.y<TopLeft.y) && (V2.y>=TopLeft.y) )
{
if(dx)
Polygon->Points[Polygon->PointsCount].x=V1.x+(TopLeft.y-V1.y)/m;
else
Polygon->Points[Polygon->PointsCount].x=V1.x;
Polygon->Points[Polygon->PointsCount++].y=TopLeft.y;
Polygon->Points[Polygon->PointsCount++]=V2;
}
}

/*
=======================
剪貼下邊緣
=======================
*/

void CLIP_Bottom(Polygon2D_t *Polygon,_2D V1,_2D V2)
{
float dx,dy, m=1;
INIT_CLIP
// ************OK************
if ( (V1.y<=DownRight.y) && (V2.y<=DownRight.y) )
Polygon->Points[Polygon->PointsCount++]=V2;
// *********LEAVING**********
if ( (V1.y<=DownRight.y) && (V2.y>DownRight.y) )
{
if(dx)
Polygon->Points[Polygon->PointsCount].x=V1.x+(DownRight.y-V1.y)/m;
else
Polygon->Points[Polygon->PointsCount].x=V1.x;
Polygon->Points[Polygon->PointsCount++].y=DownRight.y;
}
// ********ENTERING*********
if ( (V1.y>DownRight.y) && (V2.y<=DownRight.y) )
{
if(dx)
Polygon->Points[Polygon->PointsCount].x=V1.x+(DownRight.y-V1.y)/m;
else
Polygon->Points[Polygon->PointsCount].x=V1.x;
Polygon->Points[Polygon->PointsCount++].y=DownRight.y;
Polygon->Points[Polygon->PointsCount++]=V2;
}
}

爲了獲得完整的多邊形剪貼函數,咱們須要定義一個附加的全局變量:
polygon2D_t TmpPoly;

void CLIP_Polygon(Polygon2D_t *Polygon,Polygon2D_t *Clipped)
{
int v,d;

Clipped->PointsCount=0;
TmpPoly.PointsCount=0;

for (v=0; v<Polygon->PointsCount; v++)
{
d=v+1;
if(d==Polygon->PointsCount)d=0;
CLIP_Left(&TmpPoly, Polygon->Points[v],Polygon->Points[d]);
}
for (v=0; v<TmpPoly.PointsCount; v++)
{
d=v+1;
if(d==TmpPoly.PointsCount)d=0;
CLIP_Right(Clipped, TmpPoly.Points[v],TmpPoly.Points[d]);
}
TmpPoly.PointsCount=0;
for (v=0; v<Clipped->PointsCount; v++)
{
d=v+1;
if(d==Clipped->PointsCount)d=0;
CLIP_Top(&TmpPoly, Clipped->Points[v],Clipped->Points[d]);
}
Clipped->PointsCount=0;
for (v=0; v<TmpPoly.PointsCount; v++)
{
d=v+1;
if(d==TmpPoly.PointsCount)d=0;
CLIP_Bottom(Clipped, TmpPoly.Points[v],TmpPoly.Points[d]);
}
}

程序原理同Z-剪貼同樣,因此咱們能夠輕鬆地領會它。


隱面消除
Dilemna
底面消除
Z-緩衝
The Dilemna
三維引擎的核心是它的HSR系統,因此咱們必須考慮選擇那一種。通常來講,最流行
的幾種算法是:
畫筆算法
須要的時間增加更快
難以實現(尤爲重疊測試)
不能準確地排序複雜的場景
字節空間分區樹
特別快
難以實現
僅僅能排序靜態多邊形
須要存儲樹
Z-緩存
須要的時間隨多邊形的數目線性地增長
在多邊形大於5000後速度比畫筆算法快
可以完美地渲染任何場景,即便邏輯上不正確
很是容易實現
簡單
須要大量的內存
很慢
因此咱們的選擇是Z-緩存。固然也能夠選擇其餘算法。

底面消除
除了這些方法,咱們能夠很容易地消除多邊形的背面來節省大量的計算時間。首先
咱們定義一些有用的函數來計算平面和法向量以及填充。而後,咱們給這個函數增長
紋理和陰影計算。這些變量爲全局變量:
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;
}


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。
平面公式是:

Ax + By + Cz + 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;


3D紋理映射
概述
魔幻數字
紋理映射的透視修正

概述
在作紋理映射時首先考慮的是創建紋理數組和初始化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; v<Object->VertexCount; v++)
VEC_MultMatrix(&Object->Vertex[v].Local,matrix,&Object->Vertex[v].World);
for(p=0; p<Object->PolygonCount; 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; v<Object->VertexCount; v++)
VEC_MultMatrix(&Object->Vertex[v].World,matrix,&Object->Vertex[v].Aligned);
for(p=0; p<Object->PolygonCount; p++)
{
VEC_MultMatrix(&Object->Polygon[p].P.World,matrix,&Object->Polygon[p].P.Aligned);
VEC_MultMatrix(&Object->Polygon[p].M.World,matrix,&Object->Polygon[p].M.Aligned);
VEC_MultMatrix(&Object->Polygon[p].N.World,matrix,&Object->Polygon[p].N.Aligned);
}
}

魔幻數

既然咱們已經獲得了變幻的紋理座標,咱們的目標是發如今紋理位圖上的像素
水平和垂直的座標在屏幕上如何繪製。紋理座標稱爲u和v。下面的公式給出座標:
u = a * TEXTURE_SIZE / c


v = b * TEXTURE_SIZE / c


a,b,c知足下面的等式:
a = Ox + Vx j + Hx i

b = Oy + Vy j + Hy i

c = Oz + Vz j + Hz i


其中O,H,V數是魔幻數。它根據下面的公式由紋理座標計算獲得:
Ox = NxPy - NyPx
Hx = NyPz - NzPy
Vx = NzPx - NxPz
Oy = MxPy - MyPx
Hy = MyPz - MzPy
Vy = MzPx - MxPz
Oz = MyNx - MxNy
Hz = MzNy - MyNz
Vz = MxNz - MzNx

這裏,咱們不解釋魔幻數的緣由。它看起來像奇怪的叉積。


紋理映射透視修正
O,H,V數的計算須要一些修正,因此咱們增長下面到ENG3D_SetPlane:
//用於修正當數字變得太大時候的錯誤
#define FIX_FACTOR 1/640

//初始化紋理矢量
P=Polygon->P.Aligned;
M=VEC_Difference(Polygon->M.Aligned,Polygon->P.Aligned);
N=VEC_Difference(Polygon->N.Aligned,Polygon->P.Aligned);

P.x*=Focal_Distance;
P.y*=Focal_Distance;

M.x*=Focal_Distance;
M.y*=Focal_Distance;

N.x*=Focal_Distance;
N.y*=Focal_Distance;

下面是VEC_Difference的實現:
_3D VEC_Difference(_3D Vector1, _3D Vector2)
{
return P3D(Vector1.x-Vector2.x,Vector1.y-Vector2.y,Vector1.z-Vector2.z);
}

而後計算魔幻數。
_3D O, H, V;
ENG3D_SetPlane:
H.x=(N.y*P.z-N.z*P.y)*FIX_FACTOR;
V.x=(N.z*P.x-N.x*P.z)*FIX_FACTOR;
O.x=(N.x*P.y-N.y*P.x)*FIX_FACTOR;

H.z=(M.z*N.y-M.y*N.z)*FIX_FACTOR;
V.z=(M.x*N.z-M.z*N.x)*FIX_FACTOR;
O.z=(M.y*N.x-M.x*N.y)*FIX_FACTOR;

H.y=(M.y*P.z-M.z*P.y)*FIX_FACTOR;
V.y=(M.z*P.x-M.x*P.z)*FIX_FACTOR;
O.y=(M.x*P.y-M.y*P.x)*FIX_FACTOR;
下面爲TEX_HLine改變VID_HLine以便使用紋理映射(難以理解的部分),首先咱們
必須初始化魔幻數座標:
a=-((long)O.x+((long)V.x*(long)j)+((long)H.x*(long)i))*64L;
b= ((long)O.y+((long)V.y*(long)j)+((long)H.y*(long)i))*64L;
c= ((long)O.z+((long)V.z*(long)j)+((long)H.z*(long)i));
long Hx,Hy,Hz;
int u,v;
BYTE color=0;
BYTE *mapping=Textures[texture].Picture;
而後把H.x 和 H.y乘以64,這樣咱們不須要爲每個像素作計算。咱們用長整形數代替
浮點數:
Hx=H.x*-64;
Hy=H.y*64;
Hz=H.z;

對於每個像素,改變最後一個參數而且替代繪製原來的參數:
if(c)
{
u=a/c;
v=b/c;
color=mapping[((v&63)<<6)+(u&63)];
if(color)
{
zbuffer[offset]=z;
SCREENBUFFER[offset]=LightTable[light][color];
}
}
a+=Hx;
b+=Hy;
c+=Hz;

如今咱們獲得本身的紋理映射!




三維明暗
計算法向量
計算叉乘
使用光線表

計算法向量
在3D數學教程裏咱們已經深刻討論了矢量和法向量,這裏咱們給出一些實現:
float VEC_DotProduct(_3D Vector1, _3D Vector2)
{
return (Vector1.x*Vector2.x+Vector1.y*Vector2.y+Vector1.z*Vector2.z);
}

_3D VEC_CrossProduct(_3D Vector1, _3D Vector2)
{
return P3D(Vector1.y*Vector2.z-Vector1.z*Vector2.y,Vector1.z*Vector2.x-Vector1.x*Vector2.z,Vector1.x*Vector2.y-Vector1.y*Vector2.x);
}
void VEC_Normalize(_3D * Vector)
{
float m=sqrt(Vector->x*Vector->x+Vector->y*Vector->y+Vector->z*Vector->z);
Vector->x/=m;
Vector->y/=m;
Vector->z/=m;
}

對於3D明暗,須要向ENG3D_SetPlane增長:
//計算平面的法向量
PlaneNormal=VEC_CrossProduct(P3D(x2-x1,y2-y1,z2-z1),P3D(x3-x1,y3-y1,z3-z1));
VEC_Normalize(&PlaneNormal);

計算叉積
正如在數學部分所說的,兩個矢量間的夾角等於他們的點積(當他們歸一化後)。
爲發現到達平面的光線,咱們簡單地把環境光和歸一化的光源的世界座標系的叉積
與最大的光強度的乘積相加。下面是代碼:

全局變量定義:
WORD AmbientLight=20;
#define MAXLIGHT 32
static Vertex_t LightSource;
WORD light;

在SetPlane函數裏:
//計算法向量和光源的點積的強度
light=MAXLIGHT*VEC_DotProduct(PlaneNormal,LightSource.World)+AmbientLight;
if(light>MAXLIGHT)light=MAXLIGHT;
if(light<1)light=1;


明顯地,咱們須要初始化光源,正如計算法向量頂點。
//初始化光源
LightSource.Local=P3D(0,0,0);
MAT_Identity(matrix);
TR_Translate(matrix, 10,10,10);
TR_Rotate(matrix, 0,128-32,128);
VEC_MultMatrix(&LightSource.Local,matrix,&LightSource.World);
VEC_Normalize(&LightSource.World);

使用光線表

光線表是在基於調色板技術上使用光線強度的一種方法。在每個強度上能夠發現
最好的顏色匹配。Christopher Lampton在他的幻想花園一書中設計了大量的光源表
發生器。不幸的是,他使用的是本身的格式,因此咱們能夠選擇本身的或他人的光線表。一旦咱們已經有了光線表,一旦咱們有了廣西表,就能夠在全局數組中加載它。
BYTE LightTable[MAXLIGHT+1][256];
一旦已經加載,咱們在TEX_HLine函數中改變以下:
screen[offset]=LightTable[light][color];
咱們獲得了三維明暗。
3D簡介
   咱們首先從座標系統開始。你也許知道在2D裏咱們常用Ren?笛卡兒座標系統在平面上來識別點。咱們使用二維(X,Y):X表示水平軸座標,Y表示縱軸座標。在3維座標系,咱們增長了Z,通常用它來表示深度。因此爲表示三維座標系的一個點,咱們用三個參數(X,Y,Z)。這裏有不一樣的笛卡兒三維繫統可使用。可是它們都是左手螺旋或右手螺旋的。右手螺旋是右手手指的捲曲方向指向Z軸正方向,而大拇指指向X軸正方向。左手螺旋是左手手指的捲曲方向指向Z軸負方向。實際上,咱們能夠在任何方向上旋轉這些座標系,並且它們仍然保持自己的特性。在計算機圖形學,經常使用座標系爲左手座標系,因此咱們也使用它。:

X 正軸朝右
Y 正軸向上
Z 正軸指向屏幕裏

矢量
什麼是矢量?幾句話,它是座標集合。首先咱們從二維矢量開始,(X,Y):例如矢量P(4,5)(通常,咱們用->表示矢量)。咱們認爲矢量P表明點(4,5),它是從原點指向(4,5)的有方向和長度的箭頭。咱們談論矢量的長度指從原點到該點的距離。二維距離計算公式是
| P | = sqrt( x^2 + y^2 )
這裏有一個有趣的事實:在1D(點在單一的座標軸上),平方根爲它的絕對值。讓咱們討論三維矢量:例如P(4, -5, 9),它的長度爲
| P | = sqrt( x^2 + y^2 + z^2 )
它表明在笛卡兒3D空間的一個點。或從原點到該點的一個箭頭表明該矢量。在有關操做一節裏,咱們討論更多的知識。

矩陣
開始,咱們從簡單的開始:咱們使用二維矩陣4乘4矩陣,爲何是4乘4?由於咱們在三維座標系裏並且咱們須要附加的行和列來完成計算工做。在二維座標系咱們須要3乘3矩陣。着意味着咱們在3D中有4個水平參數和4個垂直參數,一共16個。例如:
4x4單位矩陣
| 1 0 0 0 |
| 0 1 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
由於任何其它矩陣與之相乘都不改變,因此稱之爲單位陣。又例若有矩陣以下:
| 10         -7 22 45 |
| sin(a) cos(a) 34 32 |
| -35        28 17  6 |
| 45        -99 32 16 | 

有關矢量和矩陣的操做
咱們已經介紹了一些很是簡單的基本概念,那麼上面的知識與三維圖形有什麼關係呢?
本節咱們介紹3D變換的基本知識和其它的一些概念。它仍然是數學知識。咱們要討論
有關矢量和矩陣操做。讓咱們從兩個矢量和開始:

( x1 , y1 , z1 ) + ( x2 , y2 , z2 ) = ( x1 + x2 , y1 + y2 , z1 + z2 )

很簡單,如今把矢量乘於係數:

k ?( x, y, z ) = ( kx, ky, kz )
能夠把上面的公式稱爲點積,以下表示:
(x1 , y1 , z1 ) ?( x2 , y2 , z2 ) = x1x2 + y1y2 + z1z2 
實際上,兩個矢量的點積被它們的模的乘積除,等於兩個矢量夾角的餘弦。因此
cos (V ^ W) =V ?W / | V | | W |

注意"^"並不表示指數而是兩個矢量的夾角。點積能夠用來計算光線於平面的夾角,咱們在計算陰影一節裏會詳細討論。
如今討論叉乘:

( x1 , y1 , z1 ) X ( x2 , y2 , z2 ) = 
( y1z2 - z1y2 , z1x2 - x1z2 , x1y2 - y1x2 ) 

叉乘對於計算屏幕的法向量很是有用。

OK,咱們已經講完了矢量的基本概念。咱們開始兩個矩陣的和。它與矢量相加很是類似,這裏就不討論了。設I是矩陣的一行,J是矩陣的一列,(i,j)是矩陣的一個元素。咱們討論與3D變換有關的重要的矩陣操做原理。兩個矩陣相乘,並且M x N <> N x M。例如:
A 4x4矩陣相乘公式
若是 A=(aij)4x4, B=(bij)4x4, 那麼
A x B=
| S> a1jbj1 S> a1jbj2 S> a1jbj3 S> a1jbj4 | 
| |
| S> a2jbj1 S> a2jbj2 S> a2jbj3 S> a2jbj4 | 
| |
| S> a3jbj1 S> a3jbj2 S> a3jbj3 S> a3jbj4 | 
| |
| S> a4jbj1 S> a4jbj2 S> a4jbj3 S> a4jbj4 |

其中 j=1,2,3,4 

並且若是 AxB=(cik)4x4 那麼咱們能夠在一行上寫下:
cik = S>4, j=1 aijbjk
( a1, a2, a3 ) x B = 
(Sum(aibi1) + b4,1, Sum(aibi2) + b4,2, Sum(aibi3) + b4,3 )

如今,咱們能夠試着把一些矩陣乘以單位陣來了解矩陣相乘的性質。咱們把矩陣與矢量相乘結合在一塊兒。下面有一個公式把3D矢量乘以一個4x4矩陣(獲得另一個三維矢量)若是B=(bij)4x4,那麼:
( a1, a2, a3 ) x B = (S>aibi1 + b4,1, S>aibi2 + b4,2, S>aibi3 + b4,3 )>

這就是矢量和矩陣操做公式。從這裏開始,代碼與數學之間的聯繫開始清晰。

變換
咱們已經見過象這樣的公式:
t( tx, ty ): ( x, y ) ==> ( x + tx, y + ty )

這是在二維笛卡兒座標系的平移等式。下面是縮放公式:

s( k ): ( x, y ) ==> ( kx, ky )

旋轉等式:
r( q ): ( x, y ) ==> ( x cos(q) - y sin(q), x sin(q) + y cos(q) )
以上都是二維公式,在三維裏公式的形式仍然很相近。
平移公式:
t( tx, ty, tz ): ( x, y, z ) ==> ( x + tx, y + ty, z + tz )
縮放公式:
s( k ): ( x, y, z ) ==> ( kx, ky, kz )
旋轉公式(圍繞Z軸):
r( q ): ( x, y, z ) ==> ( x cos(q) - y sin(q), x sin(q) + y cos(q), z )
因此咱們能夠寫出像在二維中一樣的變換公式。咱們經過乘以變換矩陣而獲得新的矢量,新矢量將指向變換點。下面是全部三維變換矩陣:

平移(tx, ty, tz)的矩陣

| 1 0 0 0 | 
| 0 1 0 0 |
| 0 0 1 0 |
| tx ty tz 1 |


縮放(sx, sy, sz)的矩陣
| sz 0 0 0 |
| 0 sy 0 0 |
| 0 0 sx 0 |
| 0 0 0 1 |


繞X軸旋轉角q的矩陣
| 1 0 0 0 |
| 0 cos(q) sin(q) 0 |
| 0 -sin(q) cos(q) 0 |
| 0 0 0 1 |



繞Y軸旋轉角q的矩陣:
| cos(q) 0 -sin(q) 0 |
| 0 1 0 0 |
| sin(q) 0 cos(q) 0 |
| 0 0 0 1 |

繞Z軸旋轉角q的矩陣:
| cos(q) sin(q) 0 0 |
|-sin(q) cos(q) 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |

因此咱們已經能夠結束關於變換的部分.經過這些矩陣咱們能夠對三維點進行任何變換.

平面和法向量
平面是平坦的,無限的,指向特定方向的表面能夠定義平面以下:
Ax + By + Cz + D = 0

其中 A, B, C稱爲平面的法向量,D是平面到原點的距離。咱們能夠經過計算平面上的兩個矢量的叉積獲得平面的法向量。爲獲得這兩個矢量,咱們須要三個點。P1,P2,P3逆時針排列,能夠獲得:
矢量1 = P1 - P2



矢量2 = P3 - P2


計算法向量爲:
法向量 = 矢量1 X 矢量2

把D移到等式的右邊獲得:
D = - (Ax + By + Cz)



D = - (A??1.x + B??2.y + C??3.z)>

或更簡單:

D = - Normal ?P1>

可是爲計算A,B,C份量。能夠簡化操做按以下等式:
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 ( y2z3 - y3z2 ) - x2 ( y3z1 - y1z3 ) - x3 ( y1z2 - y2z1 )

三維變換
存儲座標
實現矩陣系統
實現三角法系統
建立變換矩陣
如何建立透視
變換對象

存儲座標
首先能夠編寫星空模擬代碼。那麼咱們基本的結構是什麼樣?每個對象的描述是如何存儲的?爲解決這個問題,首先咱們思考另外一個問題:咱們須要的是什麼樣的座標系?最明顯的答案是:
屏幕座標系:相對於顯示器的原點的2D座標系
本地座標系:相對於對象的原點的3D座標系 
可是咱們不要忘記變換中間用到的座標系,例如:
世界座標系:相對於3D世界的原點三維座標系
對齊(視點)座標系:世界座標系的變換,觀察者的位置在世界座標系的原點。

下面是座標的基本結構:

// 二維座標
typedef struct
{
    short x, y;
}_2D;

//三維座標
typedef struct
{
    float x, y, z;
}_3D; 

這裏,咱們定義了稱爲頂點的座標結構。由於「頂點」一詞指兩個或兩個以上菱形邊的
交點。咱們的頂點能夠簡單地認爲是描述不一樣系統的矢量。

//不一樣的座標系的座標
typedef struct
{
    _3D Local;
    _3D World;
    _3D Aligned;
}Vertex_t;

實現矩陣系統
咱們須要存儲咱們的矩陣在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])
{
    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];
}
//mat1----矩陣1
//mat2----矩陣2
//dest----相乘後的新矩陣

如今你明白了嗎?如今咱們設計矢量與矩陣相乘的公式。
void VEC_MultMatrix(_3D *Source,float mat[4][4],_3D *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];
}
//Source-----源矢量(座標)
//mat--------變換矩陣
//Dest-------目標矩陣(座標)


咱們已經獲得了矩陣變換函數,不錯吧!!
//注意,這裏的矩陣變換與咱們學過的矩陣變換不一樣
//通常的,Y=TX,T爲變換矩陣,這裏爲Y = XT,
//因爲矩陣T爲4x4矩陣

實現三角法系統
幾乎每個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);
    }
}

創建變換矩陣
下面使用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;
}
//定義單位陣

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);
}
//tx,ty.tz------平移參數
//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);
}
//矩陣縮放

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);
}
//ax------繞X軸旋轉的角度
//ay------繞Y軸旋轉的角度
//az------繞Z軸旋轉的角度
//矩陣旋轉

如何創建透視
如何創建對象的立體視覺,即顯示器上的一些事物看起來離咱們很近,而另一些事物離咱們很遠。透視問題一直是困繞咱們的一個問題。有許多方法被使用。咱們使用的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
//定義焦點距離
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;
}
//獲得屏幕上的投影座標
由於0不能作除數,因此對z進行判斷。


變換對象
既然咱們已經掌握了全部的變換頂點的工具,就應該瞭解須要執行的主要步驟。 
1、初始化每個頂點的本地座標
2、設置全局矩陣爲單位陣
3、根據對象的尺寸縮放全局矩陣
4、根據對象的角度來旋轉全局矩陣
5、根據對象的位置移動全局矩陣
6、把本地座標乘以全局矩陣來獲得世界座標系
7、設置全局矩陣爲單位陣
8、用觀測者的位置的負值平移全局矩陣
9、用觀測者的角度的負值旋轉全局矩陣 
10、把世界座標系與全局矩陣相乘獲得對齊座標系
11、投影對齊座標系來獲得屏幕座標 
即:本地座標系-->世界座標系-->對齊座標系-->屏幕座標系


多邊形填充
多邊形結構
發現三角形
繪製三角形

多邊形結構
咱們如何存儲咱們的多邊形?首先,咱們必須知道再這種狀態下多邊形是二維多邊形,並且因爲初始多邊形是三維的,咱們僅須要一個臨時的二維多邊形,因此咱們可以設置二維頂點的最大數爲一個常量,而沒有浪費內存:

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;

發現三角形
由於繪製一個三角形比繪製任意的多邊形要簡單,因此咱們從把多邊形分割成
三頂點的形狀。這種方法很是簡單和直接:
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);
    }
}
//上面的算法,對於凹多邊形就不太適用
_____
|\ |
| \ | 
|____\|

繪製三角形
如今怎樣獲得三角形函數?咱們怎樣才能畫出每一條有關的直線,而且如何發現
每一行的起始和結實的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;

OK,第一步已經完成,若是有增量 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);
}

Sutherland-Hodgman剪貼
概述
Z-剪貼
屏幕剪貼 

概述
通常地,咱們更願意剪貼咱們的多邊形。必須靠着屏幕的邊緣剪貼,但也必須在觀察的前方(咱們不須要繪製觀察者後面的事物,當z左邊很是小時)。當咱們剪貼一個多邊形,並不考慮是否每個點在限制之內,而咱們更願意增長必須的頂點,因此咱們須要一個第三個多邊形結構:
typedef struct
{
    int Count;
    _3D Vertex[20];
}CPolygon_t;

因爲咱們有附加的頂點來投影,咱們再也不投影頂點,而是投影剪貼的3D多邊形到
2D多邊形。
void M3D_Project(CPolygon_t *Polygon,Polygon2D_t *Clipped,int focaldistance)
{
    int v;
    for(v=0; v<Polygon->Count; v++)
    {
        if(!Polygon->Vertex[v].z)Polygon->Vertex[v].z++;
        Clipped->Points[v].x=Polygon->Vertex[v].x*focaldistance/Polygon->Vertex[v].z+Origin.x;
        Clipped->Points[v].y=Polygon->Vertex[v].y*focaldistance/Polygon->Vertex[v].z+Origin.y;
    }
    Clipped->PointsCount=Polygon->Count;
}

Z-剪貼
首先咱們定義計算在第一個點和第二個點之間以及在第一個點和最小z值的z增量的宏。
而後,咱們計算比例,注意不要被零除。
WORD ZMin=20;
#define INIT_ZDELTAS dold=V2.z-V1.z; dnew=ZMin-V1.z;
#define INIT_ZCLIP INIT_ZDELTAS if(dold) m=dnew/dold;

咱們創建一個函數,它主要剪貼多邊形指針的參數(它將記下做爲結果的剪貼的頂點),第一個頂點(咱們剪貼的邊的開始)和第二個頂點(最後):
void CLIP_Front(CPolygon_t *Polygon,_3D V1,_3D V2)
{
    float dold,dnew, m=1;
    INIT_ZCLIP

如今咱們必須檢測邊緣是否徹底地在視口裏,離開或進入視口。若是邊緣沒有徹底地
在視口裏,咱們計算視口與邊緣的交線,用m值表示,用INIT_ZCLIP計算。

若是邊緣在視口裏: 
if ( (V1.z>=ZMin) && (V2.z>=ZMin) )
    Polygon->Vertex[Polygon->Count++]=V2;

若是邊緣正離開視口:
if ( (V1.z>=ZMin) && (V2.z<ZMin) )
{
    Polygon->Vertex[Polygon->Count ].x=V1.x + (V2.x-V1.x)*m;
    Polygon->Vertex[Polygon->Count ].y=V1.y + (V2.y-V1.y)*m;
    Polygon->Vertex[Polygon->Count++ ].z=ZMin;
}

若是邊緣正進入視口:
if ( (V1.z<ZMin) && (V2.z>=ZMin) )
{
    Polygon->Vertex[Polygon->Count ].x=V1.x + (V2.x-V1.x)*m;
    Polygon->Vertex[Polygon->Count ].y=V1.y + (V2.y-V1.y)*m;
    Polygon->Vertex[Polygon->Count++ ].z=ZMin;
    Polygon->Vertex[Polygon->Count++ ]=V2;
}
這就是邊緣Z-剪貼函數
}

如今咱們能夠寫下完整的多邊形Z-剪貼程序。爲了有表明性,定義一個宏用來
在一個對象結構中尋找適當的多邊形頂點。
#define Vert(x) Object->Vertex[Polygon->Vertex[x]]

下面是它的函數:
void CLIP_Z(Polygon_t *Polygon,Object_t *Object,CPolygon_t *ZClipped)
{
    int d,v;
    ZClipped->Count=0;
    for (v=0; v<Polygon->Count; v++)
    {
        d=v+1;
        if(d==Polygon->Count)d=0;
            CLIP_Front(ZClipped, Vert(v).Aligned,Vert(d).Aligned);
    }
}

這個函數至關簡單:它僅僅調用FrontClip函數來作頂點交換。

剪貼屏幕 
剪貼屏幕的邊緣同Z-剪貼相同,可是咱們有四個邊緣而不是一個。因此咱們須要四個
不一樣的函數。可是它們須要一樣的增量初始化:
#define INIT_DELTAS dx=V2.x-V1.x; dy=V2.y-V1.y;
#define INIT_CLIP INIT_DELTAS if(dx)m=dy/dx;

邊緣是:
_2D TopLeft, DownRight;

爲了進一步簡化_2D和 _3D結構的使用,咱們定義兩個有用的函數:
_2D P2D(short x, short y)
{
    _2D Temp;
    Temp.x=x;
    Temp.y=y;
    return Temp;
}
_3D P3D(float x,float y,float z)
{
    _3D Temp;
    Temp.x=x;
    Temp.y=y;
    Temp.z=z;
    return Temp;
}

而後使用這兩個函數來指定視口:
TopLeft=P2D(0, 0);
DownRight=P2D(319, 199);

下面是四個邊緣剪貼函數:
/*
=======================
剪貼左邊緣
=======================
*/
void CLIP_Left(Polygon2D_t *Polygon,_2D V1,_2D V2)
{
    float dx,dy, m=1;
    INIT_CLIP

    // ************OK************
    if ( (V1.x>=TopLeft.x) && (V2.x>=TopLeft.x) )
        Polygon->Points[Polygon->PointsCount++]=V2;
    // *********LEAVING**********
    if ( (V1.x>=TopLeft.x) && (V2.x<TopLeft.x) )
    {
        Polygon->Points[Polygon->PointsCount].x=TopLeft.x;
        Polygon->Points[Polygon->PointsCount++].y=V1.y+m*(TopLeft.x-V1.x);
    }
    // ********ENTERING*********
    if ( (V1.x<TopLeft.x) && (V2.x>=TopLeft.x) )
    {
        Polygon->Points[Polygon->PointsCount].x=TopLeft.x;
        Polygon->Points[Polygon->PointsCount++].y=V1.y+m*(TopLeft.x-V1.x);
        Polygon->Points[Polygon->PointsCount++]=V2;
    }
}
/*
=======================
剪貼右邊緣
=======================
*/

void CLIP_Right(Polygon2D_t *Polygon,_2D V1,_2D V2)
{
    float dx,dy, m=1;
    INIT_CLIP
    // ************OK************
    if ( (V1.x<=DownRight.x) && (V2.x<=DownRight.x) )
        Polygon->Points[Polygon->PointsCount++]=V2;
    // *********LEAVING**********
    if ( (V1.x<=DownRight.x) && (V2.x>DownRight.x) )
    {
        Polygon->Points[Polygon->PointsCount].x=DownRight.x;
        Polygon->Points[Polygon->PointsCount++].y=V1.y+m*(DownRight.x-V1.x);
    }
    // ********ENTERING*********
    if ( (V1.x>DownRight.x) && (V2.x<=DownRight.x) )
    {
        Polygon->Points[Polygon->PointsCount].x=DownRight.x;
        Polygon->Points[Polygon->PointsCount++].y=V1.y+m*(DownRight.x-V1.x);
        Polygon->Points[Polygon->PointsCount++]=V2;
    }
}
/*
=======================
剪貼上邊緣
=======================
*/
void CLIP_Top(Polygon2D_t *Polygon,_2D V1,_2D V2)
{
    float dx,dy, m=1;
    INIT_CLIP
    // ************OK************
    if ( (V1.y>=TopLeft.y) && (V2.y>=TopLeft.y) )
        Polygon->Points[Polygon->PointsCount++]=V2;
    // *********LEAVING**********
    if ( (V1.y>=TopLeft.y) && (V2.y<TopLeft.y) )
    {
        if(dx)
            Polygon->Points[Polygon->PointsCount].x=V1.x+(TopLeft.y-V1.y)/m;
        else
            Polygon->Points[Polygon->PointsCount].x=V1.x;
        Polygon->Points[Polygon->PointsCount++].y=TopLeft.y;
    }
    // ********ENTERING*********
    if ( (V1.y<TopLeft.y) && (V2.y>=TopLeft.y) )
    {
        if(dx)
            Polygon->Points[Polygon->PointsCount].x=V1.x+(TopLeft.y-V1.y)/m;
        else
            Polygon->Points[Polygon->PointsCount].x=V1.x;
        Polygon->Points[Polygon->PointsCount++].y=TopLeft.y;
        Polygon->Points[Polygon->PointsCount++]=V2;
    }
}

/*
=======================
剪貼下邊緣
=======================
*/

void CLIP_Bottom(Polygon2D_t *Polygon,_2D V1,_2D V2)
{
    float dx,dy, m=1;
    INIT_CLIP
    // ************OK************
    if ( (V1.y<=DownRight.y) && (V2.y<=DownRight.y) )
        Polygon->Points[Polygon->PointsCount++]=V2;
    // *********LEAVING**********
    if ( (V1.y<=DownRight.y) && (V2.y>DownRight.y) )
    {
        if(dx)
            Polygon->Points[Polygon->PointsCount].x=V1.x+(DownRight.y-V1.y)/m;
        else
            Polygon->Points[Polygon->PointsCount].x=V1.x;
        Polygon->Points[Polygon->PointsCount++].y=DownRight.y;
    }
    // ********ENTERING*********
    if ( (V1.y>DownRight.y) && (V2.y<=DownRight.y) )
    {
        if(dx)
            Polygon->Points[Polygon->PointsCount].x=V1.x+(DownRight.y-V1.y)/m;
        else
            Polygon->Points[Polygon->PointsCount].x=V1.x;
        Polygon->Points[Polygon->PointsCount++].y=DownRight.y;
        Polygon->Points[Polygon->PointsCount++]=V2;
    }
}

爲了獲得完整的多邊形剪貼函數,咱們須要定義一個附加的全局變量:
polygon2D_t TmpPoly;

void CLIP_Polygon(Polygon2D_t *Polygon,Polygon2D_t *Clipped)
{
    int v,d;

    Clipped->PointsCount=0;
    TmpPoly.PointsCount=0;

    for (v=0; v<Polygon->PointsCount; v++)
    {
        d=v+1;
        if(d==Polygon->PointsCount)d=0;
        CLIP_Left(&TmpPoly, Polygon->Points[v],Polygon->Points[d]);
    }
    for (v=0; v<TmpPoly.PointsCount; v++)
    {
        d=v+1;
        if(d==TmpPoly.PointsCount)d=0;
        CLIP_Right(Clipped, TmpPoly.Points[v],TmpPoly.Points[d]);
    }
    TmpPoly.PointsCount=0;
    for (v=0; v<Clipped->PointsCount; v++)
    {
        d=v+1;
        if(d==Clipped->PointsCount)d=0;
        CLIP_Top(&TmpPoly, Clipped->Points[v],Clipped->Points[d]);
    }
    Clipped->PointsCount=0;
    for (v=0; v<TmpPoly.PointsCount; v++)
    {
        d=v+1;
        if(d==TmpPoly.PointsCount)d=0;
        CLIP_Bottom(Clipped, TmpPoly.Points[v],TmpPoly.Points[d]);
    }
}

程序原理同Z-剪貼同樣,因此咱們能夠輕鬆地領會它。


隱面消除
Dilemna
底面消除
Z-緩衝 
The Dilemna
三維引擎的核心是它的HSR系統,因此咱們必須考慮選擇那一種。通常來講,最流行
的幾種算法是:
畫筆算法
須要的時間增加更快
難以實現(尤爲重疊測試)
不能準確地排序複雜的場景
字節空間分區樹
特別快
難以實現
僅僅能排序靜態多邊形
須要存儲樹
Z-緩存
須要的時間隨多邊形的數目線性地增長
在多邊形大於5000後速度比畫筆算法快
可以完美地渲染任何場景,即便邏輯上不正確
很是容易實現
簡單
須要大量的內存
很慢
因此咱們的選擇是Z-緩存。固然也能夠選擇其餘算法。

底面消除
除了這些方法,咱們能夠很容易地消除多邊形的背面來節省大量的計算時間。首先
咱們定義一些有用的函數來計算平面和法向量以及填充。而後,咱們給這個函數增長
紋理和陰影計算。這些變量爲全局變量:
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;
}


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。
平面公式是:

Ax + By + Cz + 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;


3D紋理映射
概述
魔幻數字
紋理映射的透視修正

概述
在作紋理映射時首先考慮的是創建紋理數組和初始化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; v<Object->VertexCount; v++)
        VEC_MultMatrix(&Object->Vertex[v].Local,matrix,&Object->Vertex[v].World);
    for(p=0; p<Object->PolygonCount; 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; v<Object->VertexCount; v++)
        VEC_MultMatrix(&Object->Vertex[v].World,matrix,&Object->Vertex[v].Aligned);
    for(p=0; p<Object->PolygonCount; p++)
    {
        VEC_MultMatrix(&Object->Polygon[p].P.World,matrix,&Object->Polygon[p].P.Aligned);
        VEC_MultMatrix(&Object->Polygon[p].M.World,matrix,&Object->Polygon[p].M.Aligned);
        VEC_MultMatrix(&Object->Polygon[p].N.World,matrix,&Object->Polygon[p].N.Aligned);
    }
}

魔幻數

既然咱們已經獲得了變幻的紋理座標,咱們的目標是發如今紋理位圖上的像素
水平和垂直的座標在屏幕上如何繪製。紋理座標稱爲u和v。下面的公式給出座標:
u = a * TEXTURE_SIZE / c

和 
v = b * TEXTURE_SIZE / c


a,b,c知足下面的等式:
a = Ox + Vx j + Hx i

b = Oy + Vy j + Hy i

c = Oz + Vz j + Hz i


其中O,H,V數是魔幻數。它根據下面的公式由紋理座標計算獲得:
Ox = NxPy - NyPx
Hx = NyPz - NzPy
Vx = NzPx - NxPz
Oy = MxPy - MyPx
Hy = MyPz - MzPy
Vy = MzPx - MxPz
Oz = MyNx - MxNy
Hz = MzNy - MyNz
Vz = MxNz - MzNx

這裏,咱們不解釋魔幻數的緣由。它看起來像奇怪的叉積。


紋理映射透視修正
O,H,V數的計算須要一些修正,因此咱們增長下面到ENG3D_SetPlane:
//用於修正當數字變得太大時候的錯誤
#define FIX_FACTOR 1/640

//初始化紋理矢量
P=Polygon->P.Aligned;
M=VEC_Difference(Polygon->M.Aligned,Polygon->P.Aligned);
N=VEC_Difference(Polygon->N.Aligned,Polygon->P.Aligned);

P.x*=Focal_Distance;
P.y*=Focal_Distance;

M.x*=Focal_Distance;
M.y*=Focal_Distance;

N.x*=Focal_Distance;
N.y*=Focal_Distance;

下面是VEC_Difference的實現:
_3D VEC_Difference(_3D Vector1, _3D Vector2)
{
return P3D(Vector1.x-Vector2.x,Vector1.y-Vector2.y,Vector1.z-Vector2.z);
}

而後計算魔幻數。
_3D O, H, V;
ENG3D_SetPlane: 
H.x=(N.y*P.z-N.z*P.y)*FIX_FACTOR;
V.x=(N.z*P.x-N.x*P.z)*FIX_FACTOR;
O.x=(N.x*P.y-N.y*P.x)*FIX_FACTOR;

H.z=(M.z*N.y-M.y*N.z)*FIX_FACTOR;
V.z=(M.x*N.z-M.z*N.x)*FIX_FACTOR;
O.z=(M.y*N.x-M.x*N.y)*FIX_FACTOR;

H.y=(M.y*P.z-M.z*P.y)*FIX_FACTOR;
V.y=(M.z*P.x-M.x*P.z)*FIX_FACTOR;
O.y=(M.x*P.y-M.y*P.x)*FIX_FACTOR; 
下面爲TEX_HLine改變VID_HLine以便使用紋理映射(難以理解的部分),首先咱們
必須初始化魔幻數座標:
a=-((long)O.x+((long)V.x*(long)j)+((long)H.x*(long)i))*64L;
b= ((long)O.y+((long)V.y*(long)j)+((long)H.y*(long)i))*64L;
c= ((long)O.z+((long)V.z*(long)j)+((long)H.z*(long)i));
long Hx,Hy,Hz;
int u,v;
BYTE color=0;
BYTE *mapping=Textures[texture].Picture;
而後把H.x 和 H.y乘以64,這樣咱們不須要爲每個像素作計算。咱們用長整形數代替
浮點數:
Hx=H.x*-64;
Hy=H.y*64;
Hz=H.z;

對於每個像素,改變最後一個參數而且替代繪製原來的參數:
if(c)
{
    u=a/c;
    v=b/c;
    color=mapping[((v&63)<<6)+(u&63)];
    if(color)
    {
        zbuffer[offset]=z;
        SCREENBUFFER[offset]=LightTable[light][color];
    }
}
a+=Hx;
b+=Hy;
c+=Hz;

如今咱們獲得本身的紋理映射!




三維明暗
計算法向量
計算叉乘
使用光線表

計算法向量
在3D數學教程裏咱們已經深刻討論了矢量和法向量,這裏咱們給出一些實現:
float VEC_DotProduct(_3D Vector1, _3D Vector2)
{
    return (Vector1.x*Vector2.x+Vector1.y*Vector2.y+Vector1.z*Vector2.z);
}

_3D VEC_CrossProduct(_3D Vector1, _3D Vector2)
{
    return P3D(Vector1.y*Vector2.z-Vector1.z*Vector2.y,Vector1.z*Vector2.x-Vector1.x*Vector2.z,Vector1.x*Vector2.y-Vector1.y*Vector2.x);
}
void VEC_Normalize(_3D * Vector)
{
    float m=sqrt(Vector->x*Vector->x+Vector->y*Vector->y+Vector->z*Vector->z);
    Vector->x/=m;
    Vector->y/=m;
    Vector->z/=m;
}

對於3D明暗,須要向ENG3D_SetPlane增長:
//計算平面的法向量
PlaneNormal=VEC_CrossProduct(P3D(x2-x1,y2-y1,z2-z1),P3D(x3-x1,y3-y1,z3-z1));
VEC_Normalize(&PlaneNormal);

計算叉積
正如在數學部分所說的,兩個矢量間的夾角等於他們的點積(當他們歸一化後)。
爲發現到達平面的光線,咱們簡單地把環境光和歸一化的光源的世界座標系的叉積
與最大的光強度的乘積相加。下面是代碼:

全局變量定義:
WORD AmbientLight=20;
#define MAXLIGHT 32
static Vertex_t LightSource;
WORD light;

在SetPlane函數裏:
//計算法向量和光源的點積的強度
light=MAXLIGHT*VEC_DotProduct(PlaneNormal,LightSource.World)+AmbientLight;
if(light>MAXLIGHT)light=MAXLIGHT;
if(light<1)light=1;


明顯地,咱們須要初始化光源,正如計算法向量頂點。
//初始化光源
LightSource.Local=P3D(0,0,0);
MAT_Identity(matrix);
TR_Translate(matrix, 10,10,10);
TR_Rotate(matrix, 0,128-32,128);
VEC_MultMatrix(&LightSource.Local,matrix,&LightSource.World);
VEC_Normalize(&LightSource.World);

使用光線表

光線表是在基於調色板技術上使用光線強度的一種方法。在每個強度上能夠發現
最好的顏色匹配。Christopher Lampton在他的幻想花園一書中設計了大量的光源表
發生器。不幸的是,他使用的是本身的格式,因此咱們能夠選擇本身的或他人的光線表。一旦咱們已經有了光線表,一旦咱們有了廣西表,就能夠在全局數組中加載它。
BYTE LightTable[MAXLIGHT+1][256];
一旦已經加載,咱們在TEX_HLine函數中改變以下:
screen[offset]=LightTable[light][color];
咱們獲得了三維明暗。

算法

相關文章
相關標籤/搜索