天天看點

論簡化三維流水線和逼近真實流水線快速構造引擎

Loserwang保留版權,轉載請注明出處:http://hi.baidu.com/loserwang

電子郵件:dreamcasttop   at163.com

《簡化三維流水線和逼近真實流水線》

目的:通過簡化流水線可以在相當短的實踐周期内逼近真實流水線。

工具:VC7.1、DirextX、幾何畫闆、MathType公式編輯器、科學電腦、3DS MAX 7.0、MilkShape 3D示範版等

周期:2007年8月23日至2007年8月28日

日期:2007年8月29日

論簡化三維流水線和逼近真實流水線快速構造引擎
論簡化三維流水線和逼近真實流水線快速構造引擎

圖示:在實驗中用到的真實流水線繪制的人物頭部線框模型和簡單渲染

本材料并不是一份入門參考,主要用于交流和提高,并且闡述一些比較值得關注的觀點。

第一部分、基礎設施

一、提出問題:怎樣找到一種簡單的工具來驗證三維空間的真相

大概是1年多前,2005年11月的時候,因為對圖像渲染的速度要求,放棄了當時正在使用的.Net設計平台而轉入VC圖形開發。對于二維圖形來說,高中的代數和幾何知識已經足夠了,是以當時并沒有多少障礙就能夠成熟地駕馭二維世界。對于這種“不合時宜”的做法并沒有持續很久,開始将目光投入三維圖形世界。

這一去就是1年多,這個階段并沒有累積圖形學方面的任何成功經驗,甚至連基本的DirectX應用程式都沒有寫過。僅在Windows API、Standard C++、COM、WinSocket、MFC等程式設計方面有一定的積累。在實踐中,依靠成功的程式設計能力并不總是很有效,我們總是被各種實際運算的問題所困擾。

進入三維世界,如果不是馬上能夠在實踐中演練,那要實作它幾乎不可想象,因為它包含的各種内容都有一定的抽象高度,比如說矩陣這種實用工具。矩陣真實的面目是什麼?行列裡的若幹數字?沒有應用它們是不可能體會到的:

論簡化三維流水線和逼近真實流水線快速構造引擎

圖1、一個矩陣

困擾的一個直接問題是,三維圖形的呈現有一個渲染流水線,沒有經過這個流水線而直接表達出三維圖形,比如說用約定的方式,有沒有這樣的一種可能?所有的三維圖形學書籍都迫不及待地開始了他們的理論長征,最終再簡要地給出流水線的構造步驟。或者換個角度講,這些書籍并沒有用一種簡單的模型讓我們馬上在實踐當中了解和應用大多數的理論基礎。比如說,僅僅是要呈現三維空間中的一個線段,并且初學者對三維空間理論的匮乏也僅僅能提出類似的要求。不幸的是,這些書籍都把呈現放在了真實的三維渲染流水線的模型中。在接觸到這個真實流水線之前,所有的呈現和實踐任務看起來是做不到了。而不能了解和應用這些前提要素,構造一個真實流水線的機會便變得更加遙遙無期。典型的克服這種問題的做法有二:1、純粹理論探讨,列出所有的數學公式并推導他們,這種方法被所有的大學教學系統所采納,但學習者必須付出2年的時間和接受極可能出現的失敗命運。2、在商業三維流水線系統内完成,如Direct3D和OpenGL,這種書籍充斥了廣泛的市場并且打造了大量膚淺的劣質的玩具。

D3DXMATRIX Rx, Ry;

// 建構繞x軸旋轉的矩陣,一個定值45度

D3DXMatrixRotationX(&Rx, 3.14f / 4.0f);

// 變化的繞y軸旋轉的矩陣

static float y = 3.14f / 8.0f;

D3DXMatrixRotationY(&Ry, y);

y += timeDelta;

// 标準化弧度值

if( y >= 6.28f )

y = 0.0f;

// 組合矩陣

D3DXMATRIX p = Rx * Ry;

// 旋轉了世界坐标而不是物體

Device->SetTransform(D3DTS_WORLD, &p);

如上在Direct3D中利用矩陣工具來表達三維物體的轉換是非常清晰和簡潔的。對于類似的任務,實驗中用到的代碼段:

static float angle=0;

angle+=1;

MATRIX m=MI; //對象變換矩陣

MATRIX o=MI; //流水線變換矩陣

//旋轉運動y

m=m*rotationy(angle);

//旋轉運動x

m=m*rotationx(-30);

m=m*o;

v0=v0*m;

這時候問題又來了,矩陣是什麼?它們的乘法定義又該是如何的?能夠使對象旋轉的矩陣是怎樣工作的?當學習者問起這些問題的時候,通常的答複就是對它們合了解釋一遍,如此往複。但是要幸運地遇到這樣的提問者和遇到回答者一樣困難。在盲目的實踐中追求理論的解答總是不夠利索和片面。

二、迫不及待地要在三維空間呈現,怎麼做?

實踐證明,三角學、解析幾何、立體幾何、線性代數是有效的,它們是通往三維圖形學真理方向的唯一路途。在朝空間幾何和三維圖形學的城堡邁進之前,我先詳細地回顧了這些知識,大約是2007年3月到8月間,我所作的一切事情就是它們。當然還包含了數學分析的函數、極限、微積分初步等。以至到後來,我幾乎認定整個高中數學課程就是為三維數學作準備的。比如選讀部分的矩陣行列式。其實在數學描述方面,三維數學和工程、電子等學科有着密不可分的聯系。它們幾乎可以用同一套數學描述系統來解決各種實際問題。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2、空間中的點A,表現為xy二維平面上的點A’在z次元上的位移

如上圖所示,我們可以把z方向的位移特殊處理,比如說按和x軸一定的傾斜方向表示出來,這是我們在二維圖形上直覺表示z縱深的方式。其實,這就是我們的簡化流水線的基本模型。公式如下:

論簡化三維流水線和逼近真實流水線快速構造引擎

公式1、簡化模型

它們幾乎就是簡化流水線的全部生命。當然,為了使圖形具備空間感,還需要為圖形添置對應坐标系。

這是一個非常簡單的流水線模型,但是它可以完成的任務比我們想象的要多更多。因為我們可以對三維空間中的點或向量做一系列操作,然後利用這個模型投射出結果,這樣我們就可以了解這一系列操作達到的目的以及它們是否正确。

三、GDI或者DirectDraw,基礎圖形描述工具。

Windows API提供一系列實用函數用于繪制圖形,GDI是抽象圖形裝置接口,負責直接和硬體打交道。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖3、從視窗圖形裝置建立相容裝置,并附加指定大小位圖以便繪制圖形

為了呈現動畫效果,是不能直接在視窗的抽象裝置作圖的,這樣會産生閃爍現象。因為計算機使用者會看到GDI在視窗上繪圖的全過程。這裡用到了一個概念——記憶體相容裝置。它允許我們在記憶體中畫好一桢圖形,然後一次性寫入到視窗中。

HDC hDC=GetDC(hWnd);

HDC hMemDC=CreateCompatibleDC(hDC);

HBITMAP hBmp=CreateCompatibleBitmap(hDC,800,600);

SelectBitmap(hMemDC,hBmp);

ReleaseDC(hWnd,hDC);

上面代碼表示了相容裝置建立的過程。之後就可以相當于利用視窗裝置的方式操作它,隻是沒有立即顯示而已。

//以下函數用于帶縮放地寫入目标視窗裝置

StretchBlt(

destHDC, //目标裝置

destRC.left, //目标裝置區域

destRC.top,

destRC.right,

destRC.bottom,

srcHDC, //記憶體裝置

srcRC.left, //記憶體裝置區域

srcRC.top,

srcRC.right,

srcRC.bottom,

SRCCOPY //傳送模式);

DirectDraw用了類似的手段來完成圖形呈現,但是它具備更佳的速度表現。但因為它的廣泛和實用性,加入了大量的結構用于描述各種參數和進行對應參數或者行為的操作函數而造成一定的使用難度。究其原理是相當簡單的。一份合适的DirectDraw程式樣本足夠幫我們節省很多的思考時間。我們隻要能夠建立兩個表面,進行表面複制,構造一些畫點畫線乃至寫文本函數就可以滿足需求了。

//lpsurmain是主表面指針,lpsurback是副表面指針

lpsurmain->Blt(&rectmain,lpsurback,&rectback,DDBLT_WAIT,NULL);

//DirectDraw包含有GDI模拟接口,可以像操作GDI那樣操作這個接口

int VDText(int x,int y,TCHAR* buf)

{

lpsurback->GetDC(&hMemDC);

SetBkMode(hMemDC,TRANSPARENT);

SetTextColor(hMemDC,0x00FF00);

TextOut(hMemDC,x,y,buf,(int)_tcslen(buf));

lpsurback->ReleaseDC(hMemDC);

return 0;

}

至此,我們找到了2個工具都可以幫我們完成建立繪圖表面和繪制像素。

四、開始繪制經過正交投影的線框圖形

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4、簡化模型

上圖是利用簡化模型呈現的向量直覺圖,包含兩個三維向量,以及它們的法線和一個向量到另一個向量的投影線,當然,還包括坐标軸。

原理正如公式所表述的。它的真實代碼如下:

//以視窗中心為原點,memrc.right/(2*d)是通用的縮放因子,在真實的流水線中,輸出的圖形往往不是正常螢幕大小的,這跟相機設定的投影面z=d有關系。根據90度的視角來說,投影面大小的一半正等于

POINT V(VECTOR3 vector3)

{

POINT pt;

VECTOR3 v=vector3;

v.x=v.x/v.w;

v.y=v.y/v.w;

v.z=v.z/v.w;

pt.x=(long int)(memrc.right/2+(v.x+v.z*cos(AngToRad(x_z_angle)))*memrc.right/(2*d));

pt.y=(long int)(memrc.bottom/2-(v.y+v.z*sin(AngToRad(x_z_angle)))*memrc.right/(2*d));

return pt;

}

視口的大小是受D影響的,為了“還原”螢幕的真實尺寸,必須計算出螢幕尺寸跟目前D值的縮放比。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖5、利用相似三角形表現透視投影中投影面和d的關系

回到簡化模型的讨論,計算出合适的x跟y值之後,就可以利用GDI或者DirectDraw的相關描點畫線函數來表達。呈現的結果如圖4所示。

接下來我們将引入一個更為有意義的圖形線框模型。

typedef struct

{

float x;

float y;

float z;

float w;

}VECTOR3;

上面的結構表現一個三維向量或點。其中的w分量我們先不考慮,隻要知道它為1。

//頂點清單

static VECTOR3 vertex[]=

{

{0,0,0,1},

{10,0,0,1},

{10,10,0,1},

{0,10,0,1},

{0,0,10,1},

{10,0,10,1},

{10,10,10,1},

{0,10,10,1},

{12,0,0,1},

{20,0,0,1},

{16,0,6,1},

{16,5,3,1}

};

//三角形清單

int triang[]={0,3,7,6,4,5,0,1,3,2,6,5,-1,8,10,9,11,8,10};

我們用頂點索引清單來表達三角形帶,這裡我用了一個小技巧,-1區隔不同的三角形帶。當然可以對三角形帶進行分組,甚至把它們切割為彼此獨立的三角形組。使用什麼政策就看實際情況。例如這邊的頂點索引并不是很多。

定義了頂點,接下來就要定義它們的傳輸帶了。

//對頂點進行枚舉

for(int i=2;i<sizeof(triang)/sizeof(int);i++)

{

if(triang[i]==-1)

{

         i+=2;

         continue;

}

VMove(hMemDC,vertex[triang[i-2]]*m);

VLineTo(hMemDC,vertex[triang[i-1]]*m);

VLineTo(hMemDC,vertex[triang[i]]*m);

VLineTo(hMemDC,vertex[triang[i-2]]*m);

}

傳輸帶和線框存放結構是有适應關系的。

VMove函數和VlineTo函數擷取一個三維資料,通過簡化模型将他們轉化為合适的坐标之後調用GDI的MoveToEx和LineTo函數來執行對應操作。

結果如下(我在例程中對它們作了一定的旋轉操作):

論簡化三維流水線和逼近真實流水線快速構造引擎

圖6、通過簡化模型也能夠表達複雜的物體

你會很快發現它并不具備透視特性,沒錯,這正是我們想要的。

五、點積公式的證明

論簡化三維流水線和逼近真實流水線快速構造引擎

圖7、轉移坐标,使U和x平行

論簡化三維流水線和逼近真實流水線快速構造引擎

六、矩陣——真實流水線的生命

假如世界上從來不曾存在過矩陣,我們的生活會是怎樣?對矩陣的溢美之詞可以寫一本專著。但是我們現在不打算立即描述它在三維流水線所發揮的作用。立刻就可以開始把它用于變換物體的空間位置和形變等。

typedef struct

{

float _11;float _12;float _13;float _14;

float _21;float _22;float _23;float _24;

float _31;float _32;float _33;float _34;

float _41;float _42;float _43;float _44;

}MATRIX;

代碼所示的是這樣一個矩陣:

論簡化三維流水線和逼近真實流水線快速構造引擎

矩陣乘法法則:

論簡化三維流水線和逼近真實流水線快速構造引擎

用于乘于三維向量的3*3矩陣每行可以了解為轉換後的基向量。初始基向量為

論簡化三維流水線和逼近真實流水線快速構造引擎

,分别對應x、y、z軸的标準向量。轉換後的基向量為

論簡化三維流水線和逼近真實流水線快速構造引擎

,這為我們利用變換後形态逆向構造變換矩陣提供了可能性。

以下列出繞一個軸旋轉的代碼,及其矩陣形式:

MATRIX rotationz(float angle)

{

MATRIX m;

initmatrix(m);

m._11=cos(AngToRad(angle));

m._12=sin(AngToRad(angle));

m._21=-sin(AngToRad(angle));

m._22=cos(AngToRad(angle));

m._33=1;

return m;

}

論簡化三維流水線和逼近真實流水線快速構造引擎

圖7、繞z旋轉的矩陣形式

由以上式子可以驗證“用于乘于三維向量的3*3矩陣每行可以了解為轉換後的基向量”這樣的結論。這裡列出的關于矩陣的實用操作技巧隻是很少的一部分。但不論如何,通過一定的矩陣形式變換物體坐标系裡的點坐标則一點問題都沒有。這意味着經過變換的依然是有效的三維向量資料。但是它确實産生了一定的旋轉、平移、鏡像、切變等線性變化。我們的簡化模型則對此一點也不關心。因為所有的資料變化都是在輸入到流水線之前的。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖8、旋轉等操作都是在進入流水線之前的事

七、傳說的延續,在簡化模型之上建構真實流水線,第一步是讨論方位

要逼近真實流水線,就是在去除所有約定元素之後,重新把它們建構起來。下一步可以假設整個物體空間就是世界的原點,然後對其進行透視投影。或者可以假設立體空間看到的物體就像簡化模型表現的那樣,然後對其進行物體到世界坐标系的轉換。這個轉換過程涉及到一個問題,就是物體的方位,所謂方位就是表示物體的三個坐标軸的面向的方向。

核心問題是對歐拉角(Euler)的正确了解和它是如何轉化為矩陣并最終更新物體在慣性坐标系中的朝向的。

注意歐拉角的定義:它包含3個方位資訊,Heading、Patch、Bank,物體一開始位于和慣性坐标一緻的方位。并分别繞自身的y、x、z軸作HPB旋轉。這相當于在慣性坐标系中改變物體坐标系的方位。其旋轉的角度和物體旋轉的角度相反。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖9、物體在慣性坐标系中繞自身坐标系分别做了HPB旋轉

歐拉角的直覺性在于貼近人類觀察和描述事物的方式,實際上真正了解它并不是很輕松,尤其是用數值表示出來。

(了解1)首先,假設物體始終是不動的,這樣它對于慣性坐标系或者物體坐标系來說都是不變的,根據歐拉角,定義,使物體分别繞物體坐标系旋轉一定角度,為了保持物體本身和慣性坐标系之間的不變性,以及物體和物體坐标系之間的變化關系,可以直接旋轉物體坐标系,這樣,物體和物體坐标系的相對位置改變了,但是物體和慣性坐标系的相對位置沒有改變。即,坐标系進行了HPB旋轉,而實際我們需要的是點的旋轉。即(H-)(P-)(B-),這樣便得到慣性坐标系到物體坐标系的改變。

(了解2)上面的了解有些牽強,因為并不符合我們思考的習慣。歐拉角是依據人對物體所在區域方位的認知,故對歐拉角的定義無法單獨在物體坐标系或者慣性坐标系中讨論。定義中,歐拉角HPB的旋轉均依據自身物體坐标系的軸向改變。用點的角度了解歐拉角:

論簡化三維流水線和逼近真實流水線快速構造引擎

以上是物體到慣性坐标系方位進行變換的推導過程。而慣性坐标系到物體坐标系的變化就是

論簡化三維流水線和逼近真實流水線快速構造引擎

,跟了解1的結論一緻,但顯然要合理得多。

八、三維世界的語言,三角形——頂點清單和頂點索引

九、對視平面的定義以及視平面到螢幕空間的縮放因子

論簡化三維流水線和逼近真實流水線快速構造引擎

圖10、視場大小和縮放的關系

論簡化三維流水線和逼近真實流水線快速構造引擎
論簡化三維流水線和逼近真實流水線快速構造引擎

圖11、維持螢幕寬高比的視平面調整

論簡化三維流水線和逼近真實流水線快速構造引擎

十、Z深度排序算法以及相關優化——索引拷貝、頂點拷貝以及内置排序。

排序算法測試結果:

1、 頂點拷貝

論簡化三維流水線和逼近真實流水線快速構造引擎

圖10、頂點拷貝二叉樹排序

采用二叉樹進行快速排序,但因為這種排序是位于vector之上進行的,沒有得到很好的速度支援。

2、内置排序算法以及一次性流水線(而不是每次開關GDI狀态)優化

論簡化三維流水線和逼近真實流水線快速構造引擎

圖11、内置排序

采用編譯器環境内置的排序算法qsort,它的速度可以得到很好地發揮。不過隻能傳遞數組進去,這意味着數組大小要預先設定、引擎一個常數量的資源占用,所支援的最大多邊形的大小以及速度之間的取舍。

測試結果是内置排序比架構于vector之上的快速排序快了5倍左右。

qsort(&trianglist,index+1,sizeof(TRIANGLE),tricompare);

int tricompare(const void* a,const void* b)

{

TRIANGLE t0=*((TRIANGLE*)a);

TRIANGLE t1=*((TRIANGLE*)b);

float z0avg=0.33333f*(t0.v0.z+t0.v1.z+t0.v2.z);

float z1avg=0.33333f*(t1.v0.z+t1.v1.z+t1.v2.z);

if(z0avg>z1avg)

         return -1;

if(z0avg<z1avg)

         return 1;

if(z0avg>z1avg)

         return 0;

}

論簡化三維流水線和逼近真實流水線快速構造引擎

圖12a、排序前的渲染結果

論簡化三維流水線和逼近真實流水線快速構造引擎

圖12b、排序後的渲染結果

經過排序後的渲染形體得于正常顯示。

第二部分、光照和Gouraud着色

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-1、執行了光照運算和Gouraud着色的示例

一、 燈光類型

點光源:具有顔色、位置、強度。并且随着距離變化二次反比衰減。我在模拟模型中使用一次反比衰減和固定衰減一次常量。

typedef struct

{

VECTOR3 P; //position

COLORF C; //color

float I; //intensity

}POINTLIGHT;

平行光:具有顔色、方向、強度。沒有衰減距離。強度始終為常量。

typedef struct

{

VECTOR3 V; //vector

COLORF C; //color

float I; //intensity

}INFINITELIGHT;

環境光:具有顔色、強度,沒有方向和衰減距離。強度始終為常量。

typedef struct

{

COLORF C; //color

float I; //intensity

}GLOBALLIGHT;

聚光燈:具有平行光的方向和點光源的距離衰減,以及根據照射方向發生的角度衰減。(沒有實作)

燈光清單:和多邊形流水線同級别的結構體,用于存儲有關的燈光資訊。

typedef struct

{

vector<POINTLIGHT> ptls;

vector<INFINITELIGHT> infls;

GLOBALLIGHT global;

}LIGHTS;

環境光隻有一盞,可以這樣了解,所謂的環境光是某區域場景的混色光線的簡單模拟。

二、光線加法、光線調制、Alpha混合、線性插值

光線具有紅綠藍三個分量。GDI對分量的存儲是藍綠紅分量排序。這不符合廣泛的約定。是以對顔色重新定義如下:

Typedef DWORD COLORREF3;

#define RGB3(r,g,b) ((DWORD) (((r) << 16) | ((g) << 8) | (b)))

typedef struct

{

float r;

float g;

float b;

}COLORF;

以上添置了标記“3”以示差別。接下來描述的結構體用于存儲浮點類型的顔色分量。取值從0.0f到1.0f,之是以取這個範圍是當分量之間進行乘法運算時,顔色值始終不會越界。這使得顔色乘法有意義。類似的有鏡面反射區域的餘弦值的指數運算。正方向的餘弦值始終在0到1之間,無論進行多少次次方運算都不會越界。

可以對光線分量簡單累加,這普遍适用于同性質顔色複合(如都是光線或者都是材質,注意光線和材質相加沒有意義,不過對材質的加法是假設材質資訊即将被作為光線處理,如自發光的材質等)的多數情況,當顔色值越界時簡單地截斷,因為裝置無法表現更寬廣的亮度值域。如果是光線對某種材質進行照射的情況,因為材質是對光線的影響情況,而不是自發射的光源,是以沒有可以累加的量,這時候調制運算就可以獲得有效的資料。淺色系的物體材質對光線的反射能力更強,進行調制乘法的結果也正說明這一點。

對顔色三個分量同時乘于一個相同的标量,這會改變顔色亮度。如果一個固定配額。數個顔色根據該配額比進行調制,最後累加,得出的結果是物體呈現透明化。這就是Alpha混合。

線性插值,即在一個指定的着色區域,從一種顔色變化到另一種顔色,需要對每個步進值計算顔色變化率。平滑着色和紋理仿射等均使用了線性插值算法。

三、物體材質

我們了解的物體色,是指物體表面反射白色光線後呈現的顔色。具有以下幾個分量:

環境光反射:材質的環境光反射分量決定了環境光對物體的影響程度。經常使用灰階值表示。

散射:即漫反射,物體的漫發射光線值不随觀察角度變化。但是和光線射入方向和物體表面朝向關系密切。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-2、散射強度受光線照射方向和法向量夾角影響

鏡面反射:鏡面反射是受物體表面朝向、觀察者視線、光線入射方向三方面影響的。當觀察者視線位于光線反射方向上時,鏡面反射強度最強,并依次餘弦遞減。典型的Blinn算法是根據這樣的事實:當觀察者位于光線反射方向上,觀察者視線方向跟光線入射方向的中間量為表面法線,并依次按中間值的偏移遞減。計算兩個向量的中間量的運算量要比計算投影線直到計算出反射方向的運算量少。缺點是變化率和準确的Phong模型并不一緻。但看上去足夠可信。

另外鏡面反射材質還有一個鏡面反射率的指數位标記。越大的反射率,對夾角餘弦的次方運算更為高次,衰減的速度快速下降,這樣将會得到一個足夠集中的反射區域,正如我們在現實生活中感受的那樣。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-2a、k=13

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-2b、k=138

自發光:和環境光線類似,不過實作要更加簡單,隻是一個物體發射光的參照量,如前所述,對于實體光線,需要做的運算就是光線加法。

typedef struct

{

float r;

float g;

float b;

float a;

}AMBIENT;

typedef struct

{

float r;

float g;

float b;

float a;

}DIFFUSE;

typedef struct

{

float r;

float g;

float b;

float k;

float a;

}SPECULAR;

typedef struct

{

float r;

float g;

float b;

float a;

}EMISSIVE;

Ambient:環境光反射、Diffuse:散射、Specular:鏡面反射、Emissive:自發光。

光照計算公式:

論簡化三維流水線和逼近真實流水線快速構造引擎

公式2、光照計算

對光照的了解重點在于對光線加法和調制等操作的實際使用上。當然,對向量運算的背景知識是必須的,這不是問題,不是嗎?最後,用不多的幾個光源進行合理搭配,就可以創造出良好的遊戲氛圍。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-3、鏡面反射模型

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-4a、打開了環境光

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-4b、打開了平行光

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-4c、加入了點光源

四、執行程式結構

在讨論豐富的渲染模式之前,有必要對程式的結構有一個整體清晰的認識。并且,對着色、插值、紋理、光栅化、裁剪、深度緩沖等問題的讨論将不再允許有時間對這方面的問題做一個全面而具體的闡述。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-5、程式執行框圖

主程式承載視窗和排程各個部件的協作。引擎負責像素級圖形資料的繪制和呈現工作。流水線因為其重要性,是以從主引擎結構中獨立出來,當然主觀上我們依然可以認為它們是一體的。流水線處理線上的資料,并最終轉化為可用的多邊形和色彩值,調用引擎的繪制函數寫入後備緩存。最後,主程式調用引擎的呈現功能,把記憶體中的圖像拷貝到螢幕上。

初始化:

相機儲存有裁剪面、視場角等資訊

typedef struct

{

EULER E;

VECTOR3 pos;

float nearclip;

float farclip;

float portang;

}CAMERA;

這些資訊被用于設定相機坐标轉換相關矩陣和螢幕縮放因子。

物體模型資訊是通過外部檔案導入的,存在着各種各樣的檔案格式。我們目前對檔案格式有如下需求:1、ASCII形式存儲,因為我們有時候需要打開修改資料。2、材質描述,這在光照和着色渲染的時候必須用到。3、可選的頂點法線,因為頂點法線可以在模型載入的時候再計算,對于有限的多邊形數,這種開銷是值得的,是以頂點法線不是必須的。4、嵌入的或者引用的紋理資訊。在ASCII文本中嵌入二進制資訊是可行的。幸運的是,我第一次接觸存儲檔案的時候,使用的msh支援以上各種情況。

MSHX1

GROUPS 1

MATERIAL 1      ; Cube

NONORMAL

GEOM 20 12

-2.060000 2.040001 2.033892 0.000000 0.000000

0 1 2

MATERIALS 1

White Matte

MATERIAL White Matte

0.941177 0.941177 0.941177 1.000000

0.745098 0.745098 0.745098 1.000000

1.000000 1.000000 1.000000 1.000000 1.280000

0.000000 0.000000 0.000000 1.000000

TEXTURES 0

該種檔案格式可以在MilkShape 3D的導出功能中找到,另外,它不支援頂點色和線框色,顔色是在材質中描述的。我寫了這種檔案格式的加載函數,讀取了有關的頂點材質資訊,以及預先計算了頂點法線。并存放在OBJ結構體中。(我已經盡量避免了大量的代碼引用,但是這些結構體資訊是必須的)

typedef struct

{

AMBIENT ambient;

DIFFUSE diffuse;

SPECULAR specular;

EMISSIVE emissive;

}MATERIAL;

typedef struct

{

vector<VECTOR3> vbuf; //頂點清單

vector<VECTOR3> nbuf; //法線清單

vector<COLORF> cbuf; //法線光照

vector<int> ibuf; //頂點索引

MATERIAL material; //材質

}MODEL;

typedef struct

{

vector<MODEL> models;

EULER E;

VECTOR3 pos;

MATRIX m;

}OBJ;

上面列出了物體、模型組、材質等的結構體。值得注意的是:1、法線光照會存入-1.0f,直到該法線被計算的時候才被填充有意義的數值,這樣每次流水線周期,法線光照都應該僅計算一次。2、物體的變換矩陣MATRIX m,之是以使用這個矩陣的理由是:無論如何這個矩陣都會被計算一次,在進入流水線之前可以對這個自變換矩陣進行操作,而不是實際資料,在流水線的開始點處該矩陣就會被執行。這樣,修改的将是流水線上拷貝的物體資料而不是原始的資料。因為整個流水線環節主程式是無權幹涉的,是以,傳入這個矩陣是有意義的。

流水線:

typedef struct

{

vector<POINTLIGHT> ptls;

vector<INFINITELIGHT> infls;

GLOBALLIGHT global;

}LIGHTS;

typedef struct

{

vector<OBJ> objects;

TRIANGLE trianglist[30000];

}PIPELINE;

目前不支援聚光燈,是以燈光清單沒有這個項目。流水線結構體放置了物體清單和多邊形數組。這是可以了解的,因為數組排序速度更快。

SetPos(teapot.pos,0,0,20);

SetEuler(teapot.E,0,-10,0);

teapot.m=MI*rotationy(angle);

pipeline.objects.push_back(teapot);

這是放置物體到流水線的示例

POINTLIGHT ptl;

EMISSIVE& emi=ball.models[0].material.emissive;

COLORF c={emi.r,emi.g,emi.b};

ptl.C=c;

ptl.I=30.0f;

ptl.P=ball.pos;

lights.ptls.push_back(ptl);

放置點光源到光源表的示例。其中較為有趣的部分是:我讀取了場景中一個自發光球體的自發光資訊,并把它用于光源上,球體的位置資訊也被用來描述點光源的位置,這樣,我們得到了一個“真正發光”的自發光體,至少看起來是:

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-6、運動的球體“發射”了光線

最後用一段流水線代碼來結束程式結構的讨論,用語言完整描述它們的方式是很糟糕的。

int pipelinerender(const CAMERA& cam)

{

    MATRIX cm=MI; //相機變換矩陣

    VECTOR3 campos=cam.pos;

    cm=cm*translation(-campos.x,-campos.y,-campos.z);

    cm=cm*MT(objecttoinertial(cam.E));

    //透視投影

    cm=cm*cameratoview();

    VECTOR3 v0,v1,v2,u,v,n,vavg,normal0,normal1,normal2;

    VERTEX a,b,c;

    COLORF color0,color1,color2;

    MATRIX m; //物體到世界變換矩陣

    MATRIX mi=MI; //物體到慣性變換矩陣

    MATRIX mw=MI; //慣性到世界變換矩陣

    int index=0;

    for(int p=0;p<pipeline.objects.size();p++)

    {

     OBJ& obj=pipeline.objects[p];

     vector<MODEL>& models=obj.models;

     //物體自變換和轉換到世界坐标

     mi=obj.m*objecttoinertial(obj.E);

     mw=translation(obj.pos);

     m=mi*mw;

     for(int g=0;g<obj.models.size();g++)

     {

      for(int i=0;i<models[g].ibuf.size();i+=3)

      {

       v0=models[g].vbuf[models[g].ibuf[i]]*m;

       v1=models[g].vbuf[models[g].ibuf[i+1]]*m;

       v2=models[g].vbuf[models[g].ibuf[i+2]]*m;

       u=v1-v0;

       v=v2-v0;

       n=VCross(u,v);

       n=VNormal(n);

       //背面消除

       if(backfaceclear(n,v0,cam.pos))

        continue;

       //對法向向量隻旋轉,不位移

       normal0=models[g].nbuf[models[g].ibuf[i]]*mi;

       normal1=models[g].nbuf[models[g].ibuf[i+1]]*mi;

       normal2=models[g].nbuf[models[g].ibuf[i+2]]*mi;

       //執行光照

       if(models[g].cbuf[models[g].ibuf[i]].r==-1.0f)

        models[g].cbuf[models[g].ibuf[i]]=GetFaceColor(normal0,v0,cam.pos,models[g].material,lights);

       if(models[g].cbuf[models[g].ibuf[i+1]].r==-1.0f)

        models[g].cbuf[models[g].ibuf[i+1]]=GetFaceColor(normal1,v1,cam.pos,models[g].material,lights);

       if(models[g].cbuf[models[g].ibuf[i+2]].r==-1.0f)

        models[g].cbuf[models[g].ibuf[i+2]]=GetFaceColor(normal2,v2,cam.pos,models[g].material,lights);

       color0=models[g].cbuf[models[g].ibuf[i]];

       color1=models[g].cbuf[models[g].ibuf[i+1]];

       color2=models[g].cbuf[models[g].ibuf[i+2]];

       //到相機坐标的轉換

       v0=v0*cm;

       v1=v1*cm;

       v2=v2*cm;

       //相機空間裁剪,這步操作可以提前到光照計算之前,但需要保留變換前向量

       if(cliptriangle(v0,v1,v2))

        continue;

       pipeline.trianglist[index].v0.v=v0;

       pipeline.trianglist[index].v1.v=v1;

       pipeline.trianglist[index].v2.v=v2;

       pipeline.trianglist[index].v0.color=color0;

       pipeline.trianglist[index].v1.color=color1;

       pipeline.trianglist[index].v2.color=color2;

       index++;

      }

     }

    }

    //排序

    qsort(&pipeline.trianglist,index,sizeof(TRIANGLE),tricompare);

    LockSurface();

    for(int i=0;i<index;i++)

    {

     a.v=pipeline.trianglist[i].v0.v;

     b.v=pipeline.trianglist[i].v1.v;

     c.v=pipeline.trianglist[i].v2.v;

     a.color=pipeline.trianglist[i].v0.color;

     b.color=pipeline.trianglist[i].v1.color;

     c.color=pipeline.trianglist[i].v2.color;

     VDTriang(a,b,c);

     drawcount++;

    }

    UnlockSurface();

    return index;

}

int tricompare(const void* a,const void* b)

{

    TRIANGLE t0=*((TRIANGLE*)a);

    TRIANGLE t1=*((TRIANGLE*)b);

    float z0avg=0.33333f*(t0.v0.v.z+t0.v1.v.z+t0.v2.v.z);

    float z1avg=0.33333f*(t1.v0.v.z+t1.v1.v.z+t1.v2.v.z);

    if(z0avg>z1avg)

     return -1;

    if(z0avg<z1avg)

     return 1;

    if(z0avg>z1avg)

     return 0;

}

五、平滑着色,Phong或者Gouraud

最近我一直在考慮一個問題,如果把平滑着色的代碼粘貼到文檔編輯器中,它将占據幾頁的篇幅?答案是13頁。我花了幾天時間慢慢完整這個算法。第一步我使用浮點斜率和Alpha混合插值,每秒可以執行62000多個多邊形。第二步我修改了多邊形橫向插值的内循環算法。使它不必執行乘法運算,而僅進行光線加法。為此必須給它指定浮點顔色值。每秒計算的多邊形量提高很有限,僅為63000個。第三步我放棄了浮點顔色增量,而采用類似Bresenham畫線算法的誤差累積量修正。對于顔色值大于步進值的計算我置入了循環累加。這次表現得很不錯,有90000個多邊形。後來我對浮點的斜率修正很不滿意,決定用類似的做法修改它,于是第四步我完成了整個函數的定點值計算,它處理了102000個多邊行。我還發現了一些優化的地方,顯存位置可以通過累加計算獲得,對特殊多邊形差別處理。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-7、對頂點色進行插值計算

我打算對這種算法起了一個别名,uvw三角形光栅化算法。因為整個過程都是圍繞三角形三個頂點的位置關系來進行的。除此以外都是标準的插值算法。我總是假定U是最上端的頂點。V和W的位置關系在進行x橫向顔色插值的時候起作用,我們總是從x值較小的位置開始光栅化,但是較大的V側的x值使得我們必須從W側的顔色開始插值。

Bresenham畫線算法測試x、y方向的步進距離,并以較大的步進方向作為累加誤差的軸向,而誤內插補點和較小的步進方向關聯起來。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-8、利用x和y的步進值計算誤差

如圖所示,我們在x方向上持續畫點,初始y方向的值為0,每在x方向上繪制一個像素,采用一個誤內插補點存儲y步進量的累計值,當誤內插補點大于x步進值,y方向的數值增加1個像素機關,并扣除。這樣,當我們繪制完x方向的全過程,在y方向也進行了等比例的長度修正。可能是如下的代碼:

if(lx>=ly){

err=0;

for(i=0;i<=lx;i++){

    DrawPixel(curx,cury,0x000000);

    if(err>=ly){

     err-=lx;

     cury+=y_inc;}

    err+=ly;

    curx+=x_inc;}}

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-9、y步進值比x步進值大的情況

考慮這樣一種情況,如果y方向的步進值大于x方向的步進值,這時候需要對y方向執行步進操作,利用誤內插補點修正x方向的值。在畫線任務中,這不是問題。但是在光栅化三角形的時候,情況有什麼變化?

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-10、光栅化三角形需要顧及V側和W側的斜率變化

我們可以使用類似的算法完成U到W的x方向偏移,因為x方向步進量相對于y方向的要小。但是U到V的斜率變化則超出了我們的預計,每移動y方向一個像素,x方向都會有幾個機關像素的變化。可以逆向思考,如果在x方向偏移了這幾個像素,導緻了y發生一個像素的偏移,則可以置入内循環來疊加誤差,并在誤差達到y發生偏移的量度之前退出,如此往複。代碼可能如下:

if(Vloop==false)

{

if(Verr>Ylen1)

{

    xV+=Vinc;

    Verr-=Ylen1;

}

Verr+=Vstep_;

}

else

{

while(Verr<Vstep_)

{

    xV+=Vinc;

    Verr+=Ylen1;

}

Verr-=Vstep_;

}

前半段是典型的Bresenham算法,後半段使用了一小段的Bresenham算法倒置。循環的條件不再是主動軸的跨度,而是引起被動軸發生修正的主動軸跨度。當然這個内置的倒置算法的主被動關系是相反的。即主動軸依然是y軸而不是Bresenham算法認為的x軸。

在進行這個光栅化算法的完善過程中,遇到的唯一障礙是,當V側或者W側發生斜率變化時,x的目前點已經可能産生了1個像素的誤差,若對該x值再進行計算,則以下的點均可能往某個方向偏移了一個像素。其導緻的實際效應如圖所示:

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-11、糟糕的像素誤差

我相信每個或多或少遇到此類問題的人都會馬上取消他們的周末行程。因為對于誤差的出現我們已經熟視無睹了,特别是定點運算。但是有時候多邊形最終呈現的結果很小,幾個像素大小而已,這種誤差必須避免。唯一的做法就是使用斜率變化處的頂點坐标作為起點坐标。并清除之前的偏移誤差量。

if(minY==Yuv)

{

xV=v.x;

Ylen1=abs(Yvw);

Vstep=Xvw;

Vstep_=abs(Vstep);

Vinc=Vstep>0?1:-1;

Vloop=false;

Verr=0;

if(Vstep_>Ylen1)

    Vloop=true;

}

我将此稱為“xV=v.x”啟示。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-12、xV=v.x啟示的結局

顔色插值有一緻的算法,差别隻是,xV到xW的變化量為主動軸向,像素色的變化為被動軸向。像素色分量從0到FF,每個像素增量機關為1,對應每個顔色的增量機關1,并進行軸向長度比較。

了解了基本的插值算法後,應該開始了解Gouraud如何在多邊形渲染中工作的了。

如前所述,Gouraud采用頂點顔色插值。這需要我們計算頂點的顔色值。有一個問題是,一個頂點通常被幾個多邊形共用,而多邊形經常是不共面的,是以頂點處的法向量應該是共點的幾個多邊形平面的面法線的均值,這樣才能傳回可信的光照結果。

向量的位置沒有意義,隻有它的方向才有意義。當然,所謂的方向固定意味着向量起點和終點之間位置關系的不變性。是以,事先計算各個頂點的法向量,在流水線周期内,利用旋轉變換矩陣對法向量進行操作,便可以送入光照管道。光照的位置資訊是和頂點相對坐标原點的位移聯系的。這就提出了一個要求,光照管道内的所有向量資訊都應該是歸一化的。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-13、頂點法線受周圍多邊形面法線影響

用于計算頂點法線的代碼段(Code Section):

vector<VECTOR3> normals;

VECTOR3 u,v,n,v0,v1,v2;

COLORF c;

for(int i=0;i<model.vbuf.size();i++)

{

normals.clear();

//尋找具備該頂點的多邊形

for(int j=0;j<model.ibuf.size();j+=3)

{

    if((model.ibuf[j]!=i)&&(model.ibuf[j+1]!=i)&&(model.ibuf[j+2]!=i))

     continue;

    //計算多邊形面法線

    v0=model.vbuf[model.ibuf[j]];

    v1=model.vbuf[model.ibuf[j+1]];

    v2=model.vbuf[model.ibuf[j+2]];

    u=v1-v0;

    v=v2-v0;

    n=VCross(u,v);

    normals.push_back(n);

}

//所有法線相加

for(int j=0;j<normals.size();j++)

    n=n+normals[j];

n=VNormal(n/normals.size());

model.nbuf.push_back(n);

SetColorf(c,-1.0f,-1.0f,-1.0f);

model.cbuf.push_back(c);

}

最後說明的一個原則是:在設計算法的時候,如果有進行疊代,應在疊代資料産生增量前使用資料,在使用資料前計算溢出和裁剪。次序上的混亂經常是未知錯誤的根源。如上面的斜率變化應該屬于溢出狀态,即y掃描線已經到達了限界,必須在使用資料前修改部分資料。而進行增量前使用資料則容易了解。我們不應該在0掃描線未繪制的情況下把掃描線移動到了1,這樣我們将缺乏0掃描線和增加了n+1掃描線。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖2-14、在沒有紋理貼圖參與的情況下,人物外形已經很逼真了

六、一個示例代碼

使用為數不多的自定義代碼可以建立高效靈活的執行程式,這是引擎的主要功用。它提供了大量的按約定模式組織的重用代碼段和函數集。特别是,經過封裝,可以利用顯見的和容易了解的主程式邏輯來驅動引擎,達到預想的效果。

示例代碼僅提供這些必須的資訊:模型檔案位置、相機資訊、環境燈光,渲染器則提供:物體位置,光源資訊。此外的一切任務全部在引擎中執行。在使代碼簡約、可讀性強以及格式命名等方面進行了各種權衡和取舍。為了使結構體名盡量簡單,我幾乎都是使用它們直接的明文形式,這可能會導緻重名問題,在某種必要的情況下,可以加入名稱空間(Namespace)。

CAMERA cam;

OBJ wall,robit;

OBJ ball;

OBJ ball1;

int CustomInit(HWND hWnd)

{

//設定歐拉相機位置

SetPos(cam.pos,0,50,-90);

//設定歐拉相機方位

SetEuler(cam.E,0,30,0);

cam.nearclip=20;

cam.farclip=300;

cam.portang=90;

SetCamera(cam);

Load_msh(_T("彩色牆面.msh"),wall);

Load_msh(_T("機器人-武器.msh"),robit);

Load_msh(_T("ball.msh"),ball);

Load_msh(_T("ball1.msh"),ball1);

//設定環境光

setupgloballight(rgb3tofloat(0xFFFFFF),0.42f);

return 0;

}

int CustomRender(HWND hWnd)

{

static float starttime=(float)GetTickCount();

static float timedelta=0.0f;

timedelta=GetTickCount()-starttime;

//變換角度

static float angle=0;

angle=ANormal(timedelta/1000.0f*100.0f);

SetPos(wall.pos,0,0,0);

SetEuler(wall.E,0,0,0);

wall.m=MI*rotationy(-5);

pipeline.objects.push_back(wall);

SetPos(robit.pos,0,10,0);

robit.m=MI*rotationy(-angle);

SetEuler(robit.E,0,0,0);

pipeline.objects.push_back(robit);

SetPos(ball.pos,0,0,0);

SetPos(ball1.pos,0,0,0);

ball.pos=ball.pos*translation(30,0,0)*rotationy(angle);

ball1.pos=ball1.pos*translation(0,30,0)*rotationx(angle)*translation(0,8,0);

SetEuler(ball.E,0,0,0);

SetEuler(ball1.E,0,0,0);

ball.m=MI;

ball1.m=MI;

pipeline.objects.push_back(ball);

pipeline.objects.push_back(ball1);

POINTLIGHT ptl;

EMISSIVE& emi=ball.models[0].material.emissive;

COLORF c={emi.r,emi.g,emi.b};

ptl.C=c;

ptl.I=80.0f;

ptl.P=ball.pos;

lights.ptls.push_back(ptl);

EMISSIVE& emi1=ball1.models[0].material.emissive;

COLORF c1={emi1.r,emi1.g,emi1.b};

ptl.C=c1;

ptl.I=80.0f;

ptl.P=ball1.pos;

lights.ptls.push_back(ptl);

INFINITELIGHT infl;

infl.V=VSet(0.0f,0.0f,1.0f);

SetColorf(infl.C,1.0f,1.0f,1.0f);

infl.I=0.3f;

lights.infls.push_back(infl);

int trianglecount=pipelinerender(cam);

VDEnterGDI();

TCHAR buf[55];

_stprintf(buf,_T("%.3fFPS"),ShowFps());

VDText(50,50,buf);

_stprintf(buf,_T("多邊形:%d"),trianglecount);

VDText(50,70,buf);

_stprintf(buf,_T("每秒多邊形:%d"),drawcountpersecond);

VDText(50,90,buf);

VDLeaveGDI();

return 0;

}

七、對示例代碼的維護

随着了解程度的深入和内容的持續擴充,之前一些示範代碼的格式可能過時的,最簡單的做法是,每個階段的引擎系統和相關示範對應,并作為一個整體存儲。這樣就不必為了修改那些過時的結構體和編寫風格而寫了很多不必要的維護代碼。事實證明這是合理而必要的。在進行最新内容的擴充時将盡量避免對結構體的改變,若不得不這樣做則将在階段代碼中統一維護。

八、實用的工具集合

三維模組化、格式轉換:MilkShape 3D 1.710

幾何圖形繪制:幾何畫闆4.07

電子書制作:pdfFactory Pro 3.17

圖示制作:Microangelo Toolset 6

VC助手(提供完整的智能提示):Visual AssistX 10.1.1418

公式編輯:MathType 5.2

以上工具均可在網絡上擷取。

第三部分、UV仿射貼圖、材質混合、檔案操作

論簡化三維流水線和逼近真實流水線快速構造引擎
論簡化三維流水線和逼近真實流水線快速構造引擎

圖3-1、将光照和貼圖進行了材質混合

論簡化三維流水線和逼近真實流水線快速構造引擎

圖3-2、使用z緩沖來執行像素級排序,使物體得于正确渲染

目前還沒有涉及裁剪,是以對于一些離相機過近以至發生負投影的多邊形被簡單地剔除了。視景體的其它5個面則被簡單地利用光栅化函數過濾了。這類似于硬體的工作模式,因為它足夠簡單并且可以假設我們可以在以後事先執行了裁剪,這樣光栅化函數隻是提供了判斷的開銷,這對于不執行這樣的任務的災難性後果來講是值得的。

一、Bmp位圖格式和讀取位圖資訊

為了把貼圖放置到網格上,首先要解決的問題是使用什麼圖像格式和定義它的記憶體鏡像格式。目前最簡單的圖像格式是bmp。我使用了它的8位和24位版本。8位因為資訊量不足(僅僅256個定義長度),是以引入了調色闆的形式。而24位色版本則提供了全部的RGB資訊。執行貼圖運算的時候,為了要立即得到有用的RGB資訊,調色闆索引應該在讀取位圖之後進行轉換,和24位色版本一緻。這是目前的硬體條件所允許和顯示模式所決定的。是以,所有的各種圖像格式最終都要轉化成一緻的記憶體鏡像格式。

對于檔案操作,我使用了fstream流,這是C++的标準檔案IO操作流,是STL(标準模闆庫)的一部分,現代程式設計需要程式員更快地解決問題,模闆化為這一切作了大量的工作,C++并不意味着總是使用類結構,事實上,模闆化範型程式設計正在成為C++程式設計的事實标準。設想,一個數組可能是包容字元的或者是浮點類型的資料集,而vector則替代了數組,沒有指針操作,它使用了疊代器。原始的C++語言對于類内部類型是嚴格定義好的,假設有一個Cvector類,它可能隻接受整型資料,但是vector接受任何你想要存放的類型,使用vector<type>作為類型符号。并且可以使用和指針相同的偏移模式進行疊代,它的size()子函數提供疊代限制。

fstream f;

f.open(filename.c_str(),ios_base::in|ios_base::binary);

if(!f)

{

f.close();

return -1;

}

//讀取檔案頭

f.read((char*)&bmf.bmfh,sizeof(BITMAPFILEHEADER));

f.close();

ios_base枚舉了fstream可能會用到的各種限定符,open()第二個參數需要指定打開類型,有一些有用的枚舉:

ios_base::in 讀操作    ios_base::out 寫操作 ios_base::app 追加寫入

ios_base::ate 添入 ios_base::binary 二進制 ios_base::trunc 截斷

讀寫操作子函數:

write() 寫資料     read()讀資料     get() 擷取字元/字元串

getline() 擷取一行字元串

位圖格式:Bitmap File Format

1、鏡像檔案:

#define PALETTEENTRYS 256

typedef struct

{

BITMAPFILEHEADER bmfh;

BITMAPINFOHEADER bmih;

PALETTEENTRY palette[PALETTEENTRYS];

BYTE* bits;

int size;

int width;

int height;

int bitcount;

int bpp;

int pitch;

}BITMAPFILE;

真實檔案資料不包含BYTE* bits後面的資料,那是為了通路友善而人為加上的。

2、檔案頭BITMAPFILEHEADER

typedef struct tagBITMAPFILEHEADER {

          WORD      bfType;

          DWORD     bfSize;

          WORD      bfReserved1;

          WORD      bfReserved2;

          DWORD     bfOffBits;

} BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;

有用的資料是bfType,應該為0x4d42,即“BM”,bfSize,指出檔案頭結構大小,bfOffBits:緩沖區資料的起始處相對于檔案開頭的偏移量

3、檔案資訊BITMAPINFOHEADER

typedef struct tagBITMAPINFOHEADER{

          DWORD        biSize;

          LONG         biWidth;

          LONG         biHeight;

          WORD         biPlanes;

          WORD         biBitCount;

          DWORD        biCompression;

          DWORD        biSizeImage;

          LONG         biXPelsPerMeter;

          LONG         biYPelsPerMeter;

          DWORD        biClrUsed;

          DWORD        biClrImportant;

} BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

biSize:該結構的大小,用于指出目前結構版本,biWidth:圖像寬度,biHeight:圖像高度,biPlanes:包含的調色闆數,biBitCount:色深度,biSizeImage:圖像資料區長度,biClrUsed:使用的調色闆色數。biClrImportant:主要調色闆色數。

4、調色闆單元PALETTEENTRY結構

typedef struct tagPALETTEENTRY {

      BYTE          peRed;

      BYTE          peGreen;

      BYTE          peBlue;

      BYTE          peFlags;

} PALETTEENTRY, *PPALETTEENTRY, FAR *LPPALETTEENTRY;

peRed、peGreen、peBlue,8位長度的RGB資訊,因為約定的關系,以上資訊跟GDI的RGB次序是一緻的,但是和顯示屏RGB次序颠倒,為了使用友善,需要調整R和B的位置。最後一個标志符是向系統提示調色闆的使用方式:

#define PC_RESERVED       0x01     

#define PC_EXPLICIT       0x02     

#define PC_NOCOLLAPSE     0x04     

以上三個标志符分别指定為:可變的、系統預設、自定義。基本上,我們總是使用自定義調色闆。這個标志字元檔案沒有給出,作為保留段。是以由導入函數指定。

緩沖區沒有什麼好神秘的,不同的位深的圖像有不一緻的像素占位。8位圖像占據1個位長度用于指定調色闆索引。24位圖像占據3個位長度。主要的問題在于,由于曆史原因,很多圖像的y方向掃描線是颠倒的,也就是圖像是自底向上指定的。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖3-3、圖像自底向上指定

當圖像檔案内定的高度資訊是正值,便聲明為以上形式,反之亦然。

//翻轉位圖

if(bmf.height==Abs(bmf.height))

{

BYTE* buf=(BYTE*)malloc(bmf.size);

memcpy(buf,bmf.bits,bmf.size);

memset(bmf.bits,0,bmf.size);

for(int y=0;y<bmf.height;y++)

    memcpy(&bmf.bits[y*bmf.pitch],&buf[(bmf.height-y-1)*bmf.pitch],bmf.pitch);

free(buf);

}

論簡化三維流水線和逼近真實流水線快速構造引擎

圖3-4、圖像最終呈現在視窗上

二、仿射紋理貼圖

對物體網格表面進行貼圖可以簡化成對單個多邊形執行貼圖的任務,附圖呈現的是一個多邊形,使用的采樣對象的坐标值均轉化為0-1的浮點數值,這意味着我們可以使用任何尺寸的位圖作為采樣對象,因為多邊形使用的采樣對象坐标都是一緻的。當然,在最終光栅化階段,我們需要對事實上的坐标進行插值。這一切對于網格操作和圖像選取任務都是透明的。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖3-5、使用歸一化坐标進行紋理采樣

目前考慮的是圖像空間的采樣,是以标準的插值算法是有效的。該插值過程和Gouraud着色渲染插值同步進行,我幾乎是将Gouraud的插值代碼重新拷貝了一遍,它就開始正常工作了,當然了,我修改了變量名,以便于它同采樣點對應起來。注意,光栅化階段的采樣使用的并非歸一化坐标,而是事實坐标,這樣才能有足夠的定點數進行插值。并且避免正式采樣的時候還要轉化一次坐标。

仿射紋理映射和Gouraud着色的插值計算存在的同樣一個問題是,插值過程是在圖像空間進行的,而事實上,圖像空間上的坐标點是經過透視變換的,在圖像空間線性變化的數值,在實際相機空間中受z方向的數值變化影響而呈非線性變化,變換如圖所示:

論簡化三維流水線和逼近真實流水線快速構造引擎

圖3-6、透視變換中,z值得差別使得在視平面線性變化的點在實際空間非線性變化

圖形學有一句至理名言“如果它看起來是,那它就是了”。而事實上,這種情況是可以得到更正的,根據公式y=y’(d/z’),我們可以對d/z’進行插值計算,這樣就能夠得到y值變化在圖像空間“扭曲的”而在相機視景體空間是線性的插值過程。後面将會接觸到的z緩沖算法的一個變種就是基于這樣的事實。

盡管如此,目前我仍然使用了看起來笨拙但是更容易實作和高效的圖像空間插值。當多邊形在深度上相對于視平面平行面的跨度并不是很大,即多邊形平面和視平面的傾斜夾角較小時,插值過程越精确。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖3-7、進行仿射紋理映射的結果圖,就如我們在示意圖假想的那樣

還有其它問題,第一、起點誤差,這是擴充Bresenham算法引入的起點誤差計算,我現在要處理的事情是如此之多,是以我一直沒有對它進行額外編碼。第二、雙線性過濾,對紋理采樣的時候,為了避免鋸齒現象,應當在2D空間對目前采樣點的周邊進行權重計算以确定最終像素色值,很明顯,這會占用處理周期。

三、流水線層次結構

基本層次結構:

論簡化三維流水線和逼近真實流水線快速構造引擎

對應關系:

論簡化三維流水線和逼近真實流水線快速構造引擎

其中,若法線和頂點清單同步,而不是獨立到多邊形面序列,則通過頂點索引通路法線清單和光照清單。最終權衡,我決定放棄對3DS MAX的法線多邊形面序列的相容,對頂點法線做統一處理,使得每個頂點的光照隻需計算一次來提高效率。

修改的對應關系:

論簡化三維流水線和逼近真實流水線快速構造引擎

上例中将法線和光照和頂點清單對齊,這樣每個頂點光照将隻計算一次。但這種格式需要對多邊形網格有一個約定,對于由平面組成的圖形,需要分割位置一緻但法向量不一緻的頂點。

四、Alpha混合查找政策

在讨論光線調制和Alpha混合的時候,我曾經提到,0-1的浮點取值使得光線乘法有意義并且不會導緻越界。事實上,浮點運算有時候并不是我們想要的,而且,RGB的分量隻有256的精度範圍,在32位色格式中的A值也是使用了8位存儲,使用浮點計算顯然有些誇張。實踐證明,光線乘法并不是單純的乘法計算,而是一種類似将資訊置入載波的調制過程。在對0-1的取值進行計算的時候隐含了除于1的降0位操作。由此可知,當對兩個8位數值進行調制運算時,需要對其進行降8位操作,結果數值範圍保持在8位以内。

論簡化三維流水線和逼近真實流水線快速構造引擎

如上式所示,一種更容易了解的方式是,将分量轉化成0-1的浮點數值,再将它逆乘,得到調制的結果RGB顯示格式:

R=(int)(((float)R1/(float)0xFF)*((float)R2/(float)0xFF)*(float)0xFF)=R1*R2/0xFF

顯然,得出的結論一緻,但是0-1浮點數值的相關運算是基于先驗性的假設,這是一種不确定的推導。

for(int i=0;i<=0xFF;i++)

for(int j=0;j<=0xFF;j++)

    alphatable[i][j]=i*j>>8;

上述查找表将用于支援Alpha混合計算。例如,讨論紋理映射的時候,我忽略了這樣一個細節,如何将光照的結果同貼圖色混合起來。下面就是使用該查找表進行混合的執行個體代碼:

bmpB=alphatable[bmpB][Bbase];

bmpG=alphatable[bmpG][Gbase];

bmpR=alphatable[bmpR][Rbase];

五、ASCII網格存儲結構,msh和ase

Msh是從MilkShape 3D導出的一種簡易而高效的文本存儲格式,但是它并不支援動畫網格和骨骼。而MilkShape 3D的預設文本導出格式則支援。另外,3DS MAX導出的3DS格式導入到MilkShape 3D的時候,會出現坐标系錯誤,支援的頂點數有限,貼圖坐标破壞以及其它未發現的問題,再者,直接使用3DS MAX的一些特性選項有時候會成為一種需求,總之,有必要直接從3DS MAX導出的檔案中讀取資訊。

但是3DS MAX導出的ASE檔案有幾個問題,第一,它僅支援三角形面級别的法線和貼圖坐标,而通常我們均把法線、頂點光照、貼圖坐标等同頂點清單對齊,這個問題在上面讨論流水線層次結構的時候已經提到。第二,它使用标簽描述格式而非流格式,當然我們可以使用流格式的處理手段來讀取它,但是這意味着随着對此格式檔案的内部資訊的需求的增加,将使得代碼越來越混亂,最終我考慮了這樣的政策,開發一種自定義的二進制檔案版本,用于支援對任意輸入格式的導出和檔案級緩存工作,甚至到後來直接作為外部工具對所有開發包使用到的模型進行轉換。這可以極大緩解處理ASE格式文本資訊低效的現狀。在實作上它應當沒有問題,因為這種二進制格式是我們任意給定的,為了完成這項任務,它将同流水線的層次結構對齊起來,實作廣泛的相容性。這意味着,任何導入格式在這種二進制格式沒有展現出來的特性,在流水線層次結構中也沒有對應元素。一旦确定需要某種元素,這種特性立刻就會在流水線層次結構和二進制文本格式中同步更新。這樣,我們使用了所有我們要求的外部資訊而沒有降低處理速度。另一方面,即使為了速度考慮而優化了ASE等文本格式的導入工作,上述的開發依然是必須的。隻不過我們把它提前了,以便于我們不必在文本格式的低效性上大費腦筋。

Msh檔案結構:

論簡化三維流水線和逼近真實流水線快速構造引擎

關于這種格式,優點是簡單易行,缺點是它沒有支援更多的特性。當然,對于場景編輯,這種格式已經可以滿足需求,而桢動畫則使用多檔案或者單檔案多段存儲,如果堅持要使用這種檔案格式的話。

3DS MAX标準文本導出格式ASE的檔案結構:

論簡化三維流水線和逼近真實流水線快速構造引擎

如果你正在尋找它的幾何體存放在哪的話,事實上,這不是描述具體資訊層級關系的圖示,而是類似XML的一種存儲規範,它具有獨立标簽和嵌套标簽,獨立标簽有名稱和值表,而嵌套标簽不僅可以嵌套子标簽,還可以嵌套“子嵌套标簽”,如此遞歸下去。這種格式固然帶來很大的靈活性,但是非常浪費時間。需要對它們全部解套然後利用一緻的算法,提供一個比對标簽名進行标簽取出和疊代。

*3DSMAX_ASCIIEXPORT 200

*COMMENT "AsciiExport 版本    2.00 - Wed Sep 19 22:16:39 2007"

*SCENE {

*SCENE_FILENAME "box.max"

*SCENE_FIRSTFRAME 0

*SCENE_LASTFRAME 100

*SCENE_FRAMESPEED 30

*SCENE_TICKSPERFRAME 160

*SCENE_BACKGROUND_STATIC 0.00000000 0.00000000 0.00000000

*SCENE_AMBIENT_STATIC 0.00000000 0.00000000 0.00000000

}

第二行使用了雙引号對值進行了限定,避免被分割成子單元。我們可以假設*SCENE塊還可以内嵌子塊,假如存在,便可将此子塊作為獨立單元進行遞歸分解。直到所有嵌套子塊全部被周遊。

算法很優雅,優雅的代價是慢。我可以開發一種不太優雅的較為快速的流式提取法,但是最後我決定使用檔案級緩存和轉換工具。在沒有轉換的情況下對所有導入格式進行二進制檔案級緩存。緩存盡量寫在目前檔案夾的某個子檔案夾中,僅當子檔案夾不可通路的時候才寫入系統臨時檔案夾。

但是目前我還是把該解析算法列出來。作為一種參考和改進的基礎。

元素定義:

typedef struct

{

string name;

vector<string> values;

string itembody;

}ASEITEM;

struct ASEBLOCK

{

string tag;

vector<string> values;

vector<ASEBLOCK> blocks;

vector<ASEITEM> items;

vector<string> blockbody;

};

進行資料遞歸分解:

ASEBLOCK ase_getblock(const vector<string>& lines)

{

ASEBLOCK block;

ASEBLOCK subblock;

ASEITEM item;

vector<string> strs;

vector<string> inlinebody;

int step=0;

bool bracket=false;

bool headerline=false;

vector<string> headerinfo;

for(int i=0;i<lines.size();i++)

{

    strs=split_space(lines[i]);

    //檢查行類型

    //單元模式

    if(step==0&&strs[strs.size()-1]!=_T("{"))

    {

     item.values.clear();

     item.itembody=lines[i];

     for(int j=0;j<strs.size();j++)

     {

      if(j==0)

       item.name=strs[0];

      else

       item.values.push_back(trimquot(strs[j]));

     }

     block.items.push_back(item);

    }

    //塊模式

    headerline=false;

    if(strs[strs.size()-1]==_T("{"))

    {

     if(step==0)

     {

      headerline=true;

      headerinfo=strs;

     }

     bracket=true;

     step++;

    }

    if(strs[strs.size()-1]==_T("}"))

    {

     bracket=true;

     step--;

     if(step==0)

     {

      //subblock=ase_getblock(inlinebody);

      subblock.blockbody=inlinebody;

      for(int j=0;j<headerinfo.size();j++)

      {

       if(j==0)

        subblock.tag=headerinfo[0];

       if(j>0&&j!=headerinfo.size()-1)

        subblock.values.push_back(headerinfo[j]);

      }

      block.blocks.push_back(subblock);

      inlinebody.clear();

      bracket=false;

     }

    }

    if(bracket&&(!headerline))

     inlinebody.push_back(lines[i]);

}

return block;

}

六、自定義二進制儲存格式

論簡化三維流水線和逼近真實流水線快速構造引擎

typedef struct

{

TCHAR sourcefile[64];

int modelcount;

int materialcount;

int texturecount;

}LOSFILEHEADER;

typedef struct

{

TCHAR modelname[64];

int vertexcount;

int indexcount;

int tvertexcount;

int tindexcount;

int textureindex;

int materialindex;

}LOSMODELINFO;

這是一種非常緊湊的儲存格式,并涵蓋了每個階段引擎所需的任何細節,因為它和OBJ結構體的描述基本上保持一一對應的關系。導出和導入工作都可以非常快速而簡潔地完成。

性能測試比較

論簡化三維流水線和逼近真實流水線快速構造引擎

由圖表可見,兩者完全沒有可比性,即使是讀取10兆的檔案,LOS也可以在1秒鐘左右完成全部讀取工作,相比較而言,同樣的資訊量,ASE要占據50兆左右的空間和漫長的讀取時間。

第四部分、實時渲染技術

該主題将包含以下内容:标準視景體的構造、1/W線性插值、AABB包圍盒、碰撞檢測、Alpha通道、霧化、基礎實體模型,以及一個用于示範以上内容的例子。

我閱讀了各部分目前已經積累的一些概念性東西,并着手完成了所有細節工作。其中Alpha通道、全局頂點霧、基礎實體模型是在考察了相關需求之後,完成從最初的構思到最終呈現,并且盡量提供較高的性能,以期取得良好的結果。Alpha通道和頂點霧都使用了256分辨率的Alpha深度值,這個約定使得Alpha運算過程能夠獲得最高效的實時性能。否則,實作頂點霧的效果将變得不可行,這樣從圖像呈現的角度,遊戲的可玩性将大打折扣。

論簡化三維流水線和逼近真實流水線快速構造引擎
論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-1、第二個場景不采用全局頂點霧,場景突然中止在遠裁剪面

最近我在思索很多的主題,其一、對多邊形的利用,對貼圖和效果的呈現,應該要簡化到什麼程度,才能展現出足夠宏大的場景,比如說,也許多邊形數可能不多,但是卻占據了廣大的空間,或者物體很小,但多邊形數很多,應該是用多少多邊形才足夠并且不會影響速度。其二、或者立刻停止軟體渲染的研究工作,使用硬體來加速。計算機的核心處理速度将會越來越快,軟體渲染将會在未來被重新考慮并工作得越來越好,并且有利于表達出一些奇異的物體組織模式和渲染效果,而不必借助于硬體的更新。通常,硬體将隻滿足大多數的工業需求。其三、底層的數學演算和基于充滿激情的情感邏輯推演之間應該架起怎樣的一座橋梁。比如,如果利用可行的一套數學模型描述關于什麼是幽默和悲傷的感受。我知道它們屬于不同的概念層次。但是為什麼不能說:5是個很有趣的家夥。我是說,在遊戲開發過程當中,什麼将占據主導作用?真實光照模型的數學公式或者是“把顔色的數值降下來,它們太遠了”這樣的描述情節,碰撞物互相之間的回報力到真實實體世界的等比例模組化或者“碰上了,讓物體退回5個像素吧”。我們旨在建立一個有趣的,帶有幽默感的可預期的假想世界還是建立一個現實生活空間的縮影?最終,我通過玩大量的遊戲時發現,細節的感動通常更能讓玩家沉迷。比如《格蘭蒂亞》裡面的小型物體因為玩家的碰撞而擺動,但是小型物體隻是播放了擺動的動畫和聲音,而沒有真正和玩家的身體各部件建立碰撞關系。而在射擊遊戲中(第一人稱或者第三人稱),簡單而高效的操作按鈕将使玩家立刻判斷出它是否值得玩,而不是給更多的自由度,這種毫無節制地膨脹的技術把戲。

上述的論調看似我一直在力求逃避嚴謹的數學演算的問題,有一點。在我看來,數學演算系統在能夠用5到10個公式計算出“幽默指數”之前,我大量地使用了情感機制。

一、标準視景體的建立

很多的書籍試圖将我們将要獲得的視景體空間描述成一個立方體,這實際上是不可行的,甚至到多邊形描繪階段,如果這個多邊形附帶有貼圖坐标,也是未經透視的。

标準視景體依然是個錐形體,隻不過它在Z方向上的每個截面都同相應的W值對應了起來。W=z/d,有時候我使用w作為“未經變化的”z值使用,計算頂點霧的時候我就是通過這個數值乘于d來和全局霧區間進行比較的。

另外一個需要關注的問題是d的取值,大量的研究總是假定d的取值始終為1,理由有二:其一、w=z/d=z,這樣w和x、y、z的分量的比較工作将更為直接,目前視景體視場總是以90度為基準,這使得通過縮放到基準的最大x、y值和xy截面的z值相等

論簡化三維流水線和逼近真實流水線快速構造引擎

盡管如此,我依然使用了可變的d值,因為它使得代碼在邏輯上講是準确的,這樣在所有的向量分量都要采用d作為分母。

論簡化三維流水線和逼近真實流水線快速構造引擎

上述式子我透露了一個資訊,z方向的xy截面區域将和相應的w取值進行比較,以确定是否位于視景體内。Xmax應該是x可能的最大取值。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-2、不同的xy截面,所确定的x或者y方向的視景體區間是不同的

我曾經對于标準化視景體是否必要表示懷疑。直到我将它用于物體包圍盒地剔除工作的時候才打消了這個念頭。基本上,它将視景體的一切資訊都存放在了W分量上了。其它分量為了和W對齊都做了一定的縮放工作。

論簡化三維流水線和逼近真實流水線快速構造引擎

Zoomy和Zoomx都可以了解,關鍵在于Zoomz,每個截面的w取值都是不等的。但是以下事實可以作為比較工具:

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-3、關于變換後的z和w比較的問題

如圖,當per=0時,w和近裁剪面一緻(不考慮d的影響),而z值為0,當per=1.0時,w和遠裁剪面一緻,相應的z值為w,當per落在0到1.0的内區間時,

z-w=nc*per-nc=nc*(per-1)<0,即z<0。而當nc=0且z=0時w=z,或者z=fc時,per=1.0,w=z。

這是一個有趣的現象,盡管标準化視景體沒有給出z和w的直接大小對應關系,但是它在客觀上維護了這種大小對應關系。正确了解它在更深入的使用中将會幫助很大。

沒有一本書籍寫出比以上驗證過程更接近真相的描述了,最後給出了期望的視景體變換矩陣:

論簡化三維流水線和逼近真實流水線快速構造引擎

二、透視紋理修正

當我完成了對紋理進行透視修正的代碼工作之後,我再也不想看到以前那樣子的紋理映射效果了,我竟然忍受了它們數天的時間。并且我覺得如果有人重新發明紋理映射算法的話,它首先考慮的是經過透視變換的紋理映射坐标而不是直接對它進行圖像空間插值。隻是我對Gouraud着色概念的“剽竊”影響了個人的思維。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-4、對透視投影進行線性插值造成空間扭曲,兩個面産生交疊

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-5、z緩存使用線性插值得到的深度資訊,必然造成部分平行面交疊

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-6、通過正确的深度資訊擷取,該問題被解決(使用1/w作為插值對象,後面提到)

因為紋理貼圖是定義在視景體空間的,而x、y方向的值都被透視變換了,是以隻需将紋理貼圖進行同樣的透視變換,就能夠得到紋理貼圖坐标在圖像空間的線性插值坐标,并回乘關聯的透視因子w就能夠得到實際采樣的紋理坐标。問題是,怎麼得到關聯的w的數值。這利用了1/w是關于圖像空間線性變化的事實。現在變成了驗證1/w在圖像空間是否線性變化的問題了。

如前面的所有難題一樣,我首先采用了數學的辦法對其進行證明,并把式子寫得越來越複雜,但這并不奏效。後來我經過反複論證,使用了一種極簡的辦法,即認為1/w是關于空間直線y=1在圖像空間的投影,論證如下:

論簡化三維流水線和逼近真實流水線快速構造引擎
論簡化三維流水線和逼近真實流水線快速構造引擎

通過以上事實,就獲得了投影到圖像空間的紋理貼圖坐标的線性插值過程,以及對應的1/w線性插值過程,該坐标除于1/w就得到了視景體空間的坐标。

唯一值得注意的問題是,檢驗1/w是否落在近裁剪面以外的區間。這需要和d/nearclip進行比較,當d=1時,nearclip小于1将導緻1/w可行的區間大于1,這本來不是什麼問題,但是實踐中我們通常設定了1/w深度緩沖的偏移量為1,這樣前一個循環節的深度緩沖資訊可能無法使目前循環節正常通過檢測。這個時候隻需nearclip設定不小于1的情況。假如d=100或者更大的數值,為了保持深度緩沖區間為0到1的特性,nearclip将需要大于100!這是不可接受的,是以固然引入d值在邏輯上有效,但是基于這個原因,不應當更改這個數值。或者使用更小的取值,如0.5,這樣nearclip就可以推移到0.5,但是有誰會關心這半個機關量的距離呢?

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-7、因為過大的d(這裡是100)值導緻後續桢的一部分像素無法通過檢測

三、經過改良的流水線,對象表映射和Alpha通道

随着對物體個數的量的需求的增加,對象拷貝傳入流水線開始産生瓶頸,因為必然有大部分對象因為剔除的關系而完全不會被通路到細節資訊。之前總是假定渲染對象都是在視景體内的,但是現在的引擎引入了包圍盒層次,就不應該再這樣做。況且,以後還要使用到多次渲染呢(在一個桢緩存裡面渲染多個子畫面或者多個層次畫面,用于不同視景區間的物體呈現,這廣泛用于圖形界面和其他類似應用)。

但是什麼屬性需要放在對象表映射結構上呢?直接的回答就是:在流水線深入對象細節之前的一切必須資訊。如包圍盒區間、位置、方位等,在我試圖把它們全部枚舉出來之前趕快來看一下這個動人的結構:

typedef struct

{

OBJ* obj;

EULER E;

VECTOR3 pos;

VECTOR3 move;

MATRIX m;

COLLIDEBOX collidebox;

COLLIDEBOX aabb;

int models_size;

int randid;

float fallspeed;

float speed;

bool landing;

}REFOBJ;

有一個move向量,這用于簡單的實體系統,當判斷動作不可行或者需要額外動作(物體下落、被迫回退等)時進行修正,最後附加在pos上,這時候物體才真正做出反應。Collidebox存放對象模型的碰撞盒,這是讀入模型之後就定義好的了,aabb在遊戲邏輯需要的情況下,将重新計算目前旋轉量下的軸對齊包圍盒(Axis Align Bound Box)。為了避免過度的運算開支,我使用了變換後的Collidebox頂點計算一個AABB而不是針對所有頂點,這在控制對象是人物并隻做水準方向的旋轉的情況底下很可靠。畢竟很少有人為一個橫着放的長杆子着手編寫遊戲。如果它不是人物或者拟人的什麼其他東西的話,則可以在遊戲邏輯上建議實時轉換頂點的aabb,為了性能考慮,可以加入一個忙碌拒絕政策,即每個循環将隻處理部分物體的aabb轉換。旋轉量過大的物體獲得最高的優先權。而至于場景對象的包圍盒,為了開啟動态的地形跟蹤算法,包圍盒将進行細分,在加上場景對象絕大多數情況下不做旋轉的動作,這樣通過初始的Collidebox頂點轉換的AABB就可以直接拿來用了。Randid辨別符用于碰撞排除,即物體A在周遊所有可能碰撞對象的時候将不考慮自身。Fallspeed是用在實體模型的下落速度。Speed是物體的運動速度,landing标記物體是否處于着陸狀态,若是則拒絕為Fallspeed添加重力加速度并使其為0。

目前我使用STL的vector用于仿造數組程式設計,我可不想在指針上面懂什麼腦筋,而且該映射結構已經很簡短了,無需考慮将對象映射結構的指針放入流水線渲染表裡。不過其映射的對象則使用了指針,當寫到這裡的時候我想到我完成了一個模型統一讀取子產品,該子產品将對各個放置到對象表的對象進行命名,并利用map進行鍵值映射,但是因為這個命名可以由使用者程式設計界面定義,可能存在的重名問題将導緻不可知的結果。

經過修改的流水線體:

typedef struct

{

map<string,OBJ> objects;

vector<REFOBJ> renders;

vector<BITMAPFILE> bitmaps;

TRIANGLE alphachannel[30000];

}PIPELINE;

其中包含了對象表,渲染表,位圖表和Alpha通道。對象表和位圖表在一個合适的關卡中應當隻加載一次,渲染表将每桢清空然後投入新的對象映射。Alpha通道存放了所有透明多邊形,避免和普通多邊形混雜在一起,把普通多邊形的渲染拒絕掉,采用這種政策的一個顯而易見的用場是,繪制完所有的普通多邊形,排序Alpha通道的多邊形,最後再繪制透明多邊形。如果一個透明多邊形可見,那麼它要麼直接在所有普通多邊形的前面,要麼可能被其它透明多邊形遮擋,對于前者,這沒有問題,所有的多邊形都被繪制完畢了,而對于後者,這應該也不是問題,因為已經對Alpha通道執行排序了。當然這種多邊形排序内在的交疊情況導緻排序很難完全正确完成,除非執行的是像素級别的排序或者類似的其他掃描線檢測技術。但是它們是如此的透明,以至效果還不錯。

對透明光栅化沒有什麼值得說的,因為它使用了256的分辨率,和顔色分辨率對齊起來,一切運算結果都存放到了一張二維表格上。再也沒有比這速度更快的辦法了,要麼就不要用它。

bmpB=alphatable[bmpB][alpha]+alphatable[*surbuf][invalpha];

bmpG=alphatable[bmpG][alpha]+alphatable[*(surbuf+1)][invalpha];

bmpR=alphatable[bmpR][alpha]+alphatable[*(surbuf+2)][invalpha];

上面這段Alpha混合的代碼段我曾經打算利用它來炫耀,但是為了避免被别人指責無知我就立刻放棄了該想法。

注意一個事實:插值過程永遠不會令類似色彩或者位移或者Alpha或者其他任意數值越界,若存在,則一定是代碼寫錯了,任何優秀的輸入資料和錯誤過濾都無法挽救糟糕的代碼錯誤,是以放棄這種不必要的檢測。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-8、經典回歸:一個透明材質的茶壺

最後,有一種方式可以适當補救深度排序引起的多邊形交疊情況(這是我在剛完成這篇文檔的大部分的時候想到的),因為多邊形交疊可能性的存在,更後面渲染的多邊形因為比之前的多邊形深度要大,是以被拒絕渲染,而事實上因為透明材質的特性,這些像素資訊應該被渲染到螢幕上的。這時候可以考慮這樣一種政策,不更新深度緩存,而是渲染所有比不透明物體近的像素,因為産生交疊的多邊形通常屬于同一個物體所有,它們擁有相同的透明值,而不同物體間的交疊僅在兩個物體都擁有透明材質時才發生交疊,但是這種情況可以在關卡設計時避免。還有就是當同一個物體的多邊形交疊,且交疊的多邊形的光照色彩差異較大,這也會有問題,原本是事實在後部的透明多邊形遺留更少的色彩資訊,現在則是事實位于前部的透明多邊形遺留更少的色彩資訊,但是沒有什麼比什麼都不争取就放棄更令人沮喪了。

論簡化三維流水線和逼近真實流水線快速構造引擎
論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-9、深度緩存更新和不更新渲染的差異,第二圖近似地實作了真實效果

四、起點誤差

我曾經試着利用擴充Bresenham算法對起點誤差進行估價,但是在多邊形光栅化内無法完成,因為它結合了兩個軸的修正邏輯,而不是分開處理。最後我僅僅是在畫第一個點之前就累積了一個标準誤差,使得它在合适的時刻就修正被動變化量,而不會等到所有掃描線循環完畢,少放了一個誤內插補點。擴充Bresenham則對首尾段進行平分。如上面所述,這在沒有分支判斷的情況下無法完成,要知道,這需要雙倍的代碼。

五、包圍盒層次和碰撞檢測

在大量的書籍中,這兩者都是分别被分作兩章并展開漫長的讨論的,而在這裡,僅僅是一個小節的資訊量。這說明了兩個問題:一、我使用了極為簡練的語言試圖描述各種錯綜複雜的技術和邏輯關系。二、為了對付遊戲程式設計,我們是否被日益膨脹的技術資訊蒙蔽了雙眼。也許,希望就在轉角?

這方面的工作都是我在需求的驅使下閱讀一些概念性的講述後完成全部細節的(其實,這句話也适用于之前論述到的所有方面),但已經足夠滿足目前的一部分需求。如果可能開發一個應有盡有的引擎系統,那麼就不會再有層出不窮的A引擎B引擎誕生。除非為了逗趣。

之是以細節上的建立可以重頭做起,關鍵是這些概念描述的主體是否是公開的客觀的,如果是的話,重新觀察和思量一番也無妨。比如包圍盒層次用于物體剔除是基于這樣一個事實:我看着眼前的桌子和椅子,還有上面的水果籃,我知道櫃子底下還躲着一隻貓,可是櫃子在哪呢?它在我身後,我必定看不到它,更别提那隻行事詭秘的貓了。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-10、可視化的包圍盒

我在物體模型加載的時候處理了模型的包圍盒和内部幾何體的包圍盒,關于這方面的細節沒什麼可說的。它是2D畫面中包圍框的擴充,依然是軸對齊的,隻不過多了一個次元。對這種類型資料的處理并沒有随着新的次元的增加而難度有所加大,它依然可以被壓縮到三個一維的區間進行處理。僅當多個軸向之間需要互動的時候這種難度上的變化才展現出來。一個例子是:當物體A部分和物體B在垂直于x軸的面上相交,那麼它也應該在y軸向和z軸向和物體B都相交,這樣它除了沿反方向退出外什麼也做不了。

論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-11、物體A和物體B的某個面淺相交,玩家期望它能在B表面漫遊

當物體A和物體B淺相交的時候我們會認為它們之間碰撞是那麼地不明顯,就好像根本沒發生一樣,我們不期望繼續往前沖撞過去,至少可以讓我們繞開,這意味着除了相交面的法線負方向我們不指望外,其他的方向我們還應該能夠保留自由度。這樣的需求使得三個原本邏輯分離的一維事件重新統一在立體空間了。

注意:我在引擎中加入了淺相交的滑行政策,這對于提高可玩性提供了極大的幫助。回想我們在玩一款遊戲,我們會嘗試四處跑動,并在撞上牆壁後還期望能夠從碰撞的地方緩緩地改變朝向接着往更好的地方跑去。缺失的滑行政策将導緻這樣的問題:玩家撞上牆壁,在cpu中高速運轉的碰撞系統回報給對象控制器,不可以往前,因為在這個方向撞上了,不可以往左往右往上往下,理由一樣,隻能往後退了。

從原理上來講,上述過于嚴肅的判斷政策是正确的,但是我們還是需要滑行政策,關鍵在于我們并不想在物體撞上表面後給一個回退量,第一、需要額外的代碼,但是目前的碰撞系統的代碼簡潔而高效。第二、假如玩家不幸處在兩個碰撞盒之間,那麼就會産生無休止的回退過程。第三、是突然的回退量還是緩慢供給的回退量,突然的回退量要給多少,往哪個矢量方向,陷入另一個碰撞盒怎麼辦,緩慢的回退量看似可行,但考慮這樣一個問題:玩家碰上牆壁了,試圖沿着牆壁緩慢斜行,然後繞開它,這個時候就會産生回退、碰撞、回退、碰撞的過程,如果相機此刻定位在玩家身上,那麼整個畫面就開始顫動了。如果遊戲角色的跑動不是基于統一的地圖平面而是基于地形系統的話,這個問題就會更讓人難于接受了。角色時時刻刻都處在回退值的幹擾之下而不得安甯。

另一個問題是梯度爬行政策。注意,關于碰撞檢測和實體系統的大量用詞都是我捏造的,因為我沒有也不必去考察其它的實作方案,是以使用自己定義的名稱并不奇怪。

論簡化三維流水線和逼近真實流水線快速構造引擎
論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-12、汽車試圖沿着梯度上升,結果,它做到了!

不應該讓玩家處于永恒不變的平面,梯度爬行政策是一個簡單的地形跟蹤模型,它需要滿足以下條件:

論簡化三維流水線和逼近真實流水線快速構造引擎

1、A半高位超過B頂面

2、A在B的垂直面上處于滑行狀态

3、A底部在B頂部以下

其中條件1可以指定一個期望的閥值,預設情況下為0.5,條件2對滑行狀态的判定也可以指定小于0.5的閥值,預設情況下為0.2,太大的閥值看起來不真實(A陷入B太深了,根本動彈不得)。

碰撞檢測過程分解:

論簡化三維流水線和逼近真實流水線快速構造引擎

①、A半高和B半高比較。

If(A>=B)

正方向回報力。

If(A<B)

負方向回報力。

論簡化三維流水線和逼近真實流水線快速構造引擎

②、A區間和B區間是否交疊

If(Min(A,B)的頂點位于Max(A,B)區間)

區間交疊

If(區間交疊)

使用①力回報

Else

退出

論簡化三維流水線和逼近真實流水線快速構造引擎

④、A相對于兩端點n分面和B的兩端點的比較

If(A底分面在B頂端點之上||A頂分面在B底端點之下)

淺相交

論簡化三維流水線和逼近真實流水線快速構造引擎

⑤、滑行政策:浮動平面

If(淺相交在Y軸向)

浮動平面;消除X、Z軸向力回報

論簡化三維流水線和逼近真實流水線快速構造引擎

⑥、滑動政策:邊接觸

If(淺相交在X、Z軸向)

邊接觸;消除非淺相交軸向力回報

論簡化三維流水線和逼近真實流水線快速構造引擎

⑦、梯度爬行政策(梯度跳躍)

If(A半高大于B頂端點&&A對B在Y軸向邊接觸&&A底端點小于B頂端點)

梯度跳躍(資訊傳遞給力回報W分量)

論簡化三維流水線和逼近真實流水線快速構造引擎

⑧、Y滑行政策

If((!梯度跳躍)&&邊接觸)

Y滑行政策

論簡化三維流水線和逼近真實流水線快速構造引擎

⑨、浮動修正

If(A半高>B半高&&A底端點<B頂端點&&A頂端點>B頂端點&&(!邊接觸))

浮動修正(資訊傳遞給力回報W分量)

論簡化三維流水線和逼近真實流水線快速構造引擎

⑩、高度上升

If(梯度跳躍||浮動修正)

y++

六、全局頂點霧

論簡化三維流水線和逼近真實流水線快速構造引擎
論簡化三維流水線和逼近真實流水線快速構造引擎

圖4-13、富于詩意的全局頂點霧世界

①頂點霧濃度

論簡化三維流水線和逼近真實流水線快速構造引擎

全局霧濃度等于z在fog.length區間的占位(256分辨率)

fog.length=(f-n)*fog.percnet

fog.alpha=max(min((z-fog.length)/fog.length,1),0)*0xFF

②霧化混合

Gouraud::fog=Alpha(fog.color,vertex.color,fog.alpha)

GouraudAlpha::fog=Alpha(vertex.color,screen.color,vertex.alpha)

       ->Gouraud::fog

Texture::fog=Mix(vertex.color,bmf.color)

     ->Gouraud::fog

TextureAlpha::fog=Modulate(vertex.color,bmf.color)

     ->GouraudAlpha::fog

③“貼圖->霧化”和“霧化->貼圖”的差別

霧化後貼圖,貼圖資訊将不受霧化影響。

應該使用“貼圖->霧化”過程

④“Alpha->霧化”和“霧化->Alpha”的差別

先霧化再Alpha将導緻霧顔色透明化,若霧顔色和背景色一緻,則沒有問題。

Gouraud不處理貼圖資訊,可事先計算頂點霧顔色,為避免GouraudAlpha為了先處理Alpha而對霧濃度插值的開銷,霧顔色和背景色将一緻化。

TextureAlpha無論如何都要進行霧濃度插值,故應該給出合适的霧化順序,即“Alpha->霧化”

⑤霧化和光照的差別

霧化使用Alpha混合,有限定域

光照使用逐一材質調制和求和,無限定域,最終渲染時被截斷。

七、使用玩具車在趣味的場景中進行跳躍和跑動的示範程式

打開Release目錄下的JumpCar.exe開始玩。

繼續閱讀