貝塞爾曲面
作者: David Nikdel ( ogapo@ithink.net )
這篇教程旨在介紹貝塞爾曲面,希望有比我更懂藝術(shù)的人能用她作出一些很COOL的東東并且展示給大家。教程不能用做一個(gè)完整的貝塞爾曲面庫(kù),而是一個(gè)展示概念的程序讓你熟悉曲面怎樣實(shí)現(xiàn)的。而且這不是一篇正規(guī)的文章,為了方便理解,我也許在有些地方術(shù)語(yǔ)不當(dāng);我希望大家能適應(yīng)這個(gè)。最后,對(duì)那些已經(jīng)熟悉貝塞爾曲面想看我寫的如何的,真是丟臉;-)但你要是找到任何紕漏讓我或者NeHe知道,畢竟人無(wú)完人嘛?還有,所有代碼沒(méi)有象我一般寫程序那樣做優(yōu)化,這是故意的。我想每個(gè)人都能明白寫的是什么。好,我想介紹到此為止,繼續(xù)看下文!
數(shù)學(xué)::惡魔之音::(警告:內(nèi)容有點(diǎn)長(zhǎng)~)
好,如果想理解貝塞爾曲面沒(méi)有對(duì)其數(shù)學(xué)基本的認(rèn)識(shí)是很難的,如果你不愿意讀這一部分或者你已經(jīng)知道了關(guān)于她的數(shù)學(xué)知識(shí)你可以跳過(guò)。首先我會(huì)描述貝塞爾曲線再介紹生成貝塞爾曲面。
奇怪的是,如果你用過(guò)一個(gè)圖形程序,你就已經(jīng)熟悉了貝塞爾曲線,也許你接觸的是另外的名稱。它們是畫曲線的最基本的方法,而且通常被表示成一系列點(diǎn),其中有兩個(gè)點(diǎn)與兩端點(diǎn)表示左右兩端的切線。下圖展示了一個(gè)例子。
這是最基礎(chǔ)的貝塞爾曲線(長(zhǎng)點(diǎn)的由很多點(diǎn)在一起(多到你都沒(méi)發(fā)現(xiàn)))。這個(gè)曲線由4個(gè)點(diǎn)定義,有2個(gè)端點(diǎn)和2個(gè)中間控制點(diǎn)。對(duì)計(jì)算機(jī)而言這些點(diǎn)都是一樣的,但是特意的我們通常把前后兩對(duì)點(diǎn)分別連接,因?yàn)樗麄兊倪B線與短點(diǎn)相切。曲線是一個(gè)參數(shù)化曲線,畫的時(shí)候從曲線上平均找?guī)c(diǎn)連接。這樣你可以控制曲線曲面的精度(和計(jì)算量)。最通常的方法是遠(yuǎn)距離少細(xì)分近距離多細(xì)分,對(duì)視點(diǎn),看上去總是很完好的曲面而對(duì)速度的影響總是最小。
貝塞爾曲面基于一個(gè)基本方程,其他復(fù)雜的都是基于此。方程為:
t + (1 - t) = 1
看起來(lái)很簡(jiǎn)單不是?的確是的,這是最基本的貝塞爾曲線,一個(gè)一維的曲線。你也許從術(shù)語(yǔ)中猜到,貝塞爾曲線是多項(xiàng)式形式的。從線性代數(shù)知,一個(gè)一維的多項(xiàng)式是一條直線,沒(méi)多大意思。好,因?yàn)榛痉匠虒?duì)所有t都成立,我們可以平方,立方兩邊,怎么都行,等式都是成立的,對(duì)吧?好,我們?cè)囋嚵⒎健?/p>
(t + (1-t))^3 = 1^3
t^3 + 3*t^2*(1-t) + 3*t*(1-t)^2 + (1-t)^3 = 1
這是我們最常用的計(jì)算貝塞爾曲面的方程,a)她是最低維的不需要在一個(gè)平面內(nèi)的多項(xiàng)式(有4個(gè)控制點(diǎn)),而且b)兩邊的切線互相沒(méi)有聯(lián)系(對(duì)于2維的只有3個(gè)控制點(diǎn))。那么你看到了貝塞爾曲線了嗎?呵呵,我們都沒(méi)有,因?yàn)槲疫€要加一個(gè)東西。
好,因?yàn)榉匠套筮叺扔?,可以肯定如果你把所有項(xiàng)加起來(lái)還是等于1。這是否意味著在計(jì)算曲線上一點(diǎn)時(shí)可以以此決定該用每個(gè)控制點(diǎn)的多少呢?(答案是肯定的)你對(duì)了!當(dāng)我們要計(jì)算曲線上一點(diǎn)的值我們只需要用控制點(diǎn)(表示為向量)乘以每部分再加起來(lái)。基本上我們要用0<=t<=1,但不是必要的。不明白了把?這里有函數(shù):
P1*t^3 + P2*3*t^2*(1-t) + P3*3*t*(1-t)^2 + P4*(1-t)^3 = Pnew
因?yàn)槎囗?xiàng)式是連續(xù)的,有一個(gè)很好的辦法在4個(gè)點(diǎn)間插值。曲線僅經(jīng)過(guò)P1,P4,分別當(dāng)t=1,0。
好,一切都很好,但現(xiàn)在我怎么把這個(gè)用在3D里呢?其實(shí)很簡(jiǎn)單,為了做一個(gè)貝塞爾曲面,你需要16個(gè)控制點(diǎn),(4*4),和2個(gè)變量t,v。你要做的是計(jì)算在分量v的沿4條平行曲線的點(diǎn),再用這4個(gè)點(diǎn)計(jì)算在分量t的點(diǎn)。計(jì)算了足夠的這些點(diǎn),我們可以用三角帶連接他們,畫出貝塞爾曲面。
恩,我認(rèn)為現(xiàn)在已經(jīng)有足夠的數(shù)學(xué)背景了,看代碼把!
#include <math.h> // 數(shù)學(xué)庫(kù)
#include <stdio.h> // 標(biāo)準(zhǔn)輸入輸出庫(kù)
#include <stdlib.h> // 標(biāo)準(zhǔn)庫(kù)
typedef struct point_3d { // 3D點(diǎn)的結(jié)構(gòu)
double x, y, z;
} POINT_3D;
typedef struct bpatch { // 貝塞爾面片結(jié)構(gòu)
POINT_3D anchors[4][4]; // 由4x4網(wǎng)格組成
GLuint dlBPatch; // 繪制面片的顯示列表名稱
GLuint texture; // 面片的紋理
} BEZIER_PATCH;
BEZIER_PATCH mybezier; // 創(chuàng)建一個(gè)貝塞爾曲面結(jié)構(gòu)
BOOL showCPoints=TRUE; // 是否顯示控制點(diǎn)
int divs = 7; // 細(xì)分精度,控制曲面的顯示精度
以下是一些簡(jiǎn)單的向量數(shù)學(xué)的函數(shù)。如果你是C++愛(ài)好者你可以用一個(gè)頂點(diǎn)類(保證其為3D的)。
// 兩個(gè)向量相加,p=p+q
POINT_3D pointAdd(POINT_3D p, POINT_3D q) {
p.x += q.x; p.y += q.y; p.z += q.z;
return p;
}
// 向量和標(biāo)量相乘p=c*p
POINT_3D pointTimes(double c, POINT_3D p) {
p.x *= c; p.y *= c; p.z *= c;
return p;
}
// 創(chuàng)建一個(gè)3D向量
POINT_3D makePoint(double a, double b, double c) {
POINT_3D p;
p.x = a; p.y = b; p.z = c;
return p;
}
這基本上是用C寫的3維的基本函數(shù),她用變量u和4個(gè)頂點(diǎn)的數(shù)組計(jì)算曲線上點(diǎn)。每次給u加上一定值,從0到1,我們可得一個(gè)很好的近似曲線。
求值器基于Bernstein多項(xiàng)式定義曲線,定義p(u ')為:
p(u')=∑Bni(u')Ri
這里Ri為控制點(diǎn)
Bni(u')=[ni]u'i(1-u')n-i
且00=1,[n0]=1
u'=(u-u1)/(u2-u1)
當(dāng)為貝塞爾曲線時(shí),控制點(diǎn)為4,相應(yīng)的4個(gè)Bernstein多項(xiàng)式為:
1、B30 =(1-u)3
2、B31 =3u(1-u)2
3、B32 =3u2(1-u)
4、B33 =u3
// 計(jì)算貝塞爾方程的值
// 變量u的范圍在0-1之間
POINT_3D Bernstein(float u, POINT_3D *p) {
POINT_3D a, b, c, d, r;
a = pointTimes(pow(u,3), p[0]);
b = pointTimes(3*pow(u,2)*(1-u), p[1]);
c = pointTimes(3*u*pow((1-u),2), p[2]);
d = pointTimes(pow((1-u),3), p[3]);
r = pointAdd(pointAdd(a, b), pointAdd(c, d));
return r;
}
這個(gè)函數(shù)完成共享工作,生成所有三角帶,保存在display list。我們這樣就不需要每貞都重新計(jì)算曲面,除了當(dāng)其改變時(shí)。另外,你可能想用一個(gè)很酷的效果,用MORPHING教程改變控制點(diǎn)位置。這可以做一個(gè)很光滑,有機(jī)的,morphing效果,只要一點(diǎn)點(diǎn)開(kāi)銷(你只要改變16個(gè)點(diǎn),但要從新計(jì)算)。“最后”的數(shù)組元素用來(lái)保存前一行點(diǎn),(因?yàn)槿菐枰獌尚校6遥y理坐標(biāo)由表示百分比的u,v來(lái)計(jì)算(平面映射)。
還有一個(gè)我們沒(méi)做的是計(jì)算法向量做光照。到了這一步,你基本上有2種選擇。第一是找每個(gè)三角形的中心計(jì)算X,Y軸的切線,再做叉積得到垂直與兩向量的向量,再歸一化,得到法向量。或者(恩,這是更好的方法)你可以直接用三角形的法矢(用你最喜歡的方法計(jì)算)得到一個(gè)近似值。我喜歡后者;我認(rèn)為不值得為了一點(diǎn)點(diǎn)真實(shí)感影響速度。
// 生成貝塞爾曲面的顯示列表
GLuint genBezier(BEZIER_PATCH patch, int divs) {
int u = 0, v;
float py, px, pyold;
GLuint drawlist = glGenLists(1); // 創(chuàng)建顯示列表
POINT_3D temp[4];
POINT_3D *last = (POINT_3D*)malloc(sizeof(POINT_3D)*(divs+1)); // 更具每一條曲線的細(xì)分?jǐn)?shù),分配相應(yīng)的內(nèi)存
if (patch.dlBPatch != NULL) // 如果顯示列表存在則刪除
glDeleteLists(patch.dlBPatch, 1);
temp[0] = patch.anchors[0][3]; // 獲得u方向的四個(gè)控制點(diǎn)
temp[1] = patch.anchors[1][3];
temp[2] = patch.anchors[2][3];
temp[3] = patch.anchors[3][3];
for (v=0;v<=divs;v++) { // 根據(jù)細(xì)分?jǐn)?shù),創(chuàng)建各個(gè)分割點(diǎn)額參數(shù)
px = ((float)v)/((float)divs);
// 使用Bernstein函數(shù)求的分割點(diǎn)的坐標(biāo)
last[v] = Bernstein(px, temp);
}
glNewList(drawlist, GL_COMPILE); // 創(chuàng)建一個(gè)新的顯示列表
glBindTexture(GL_TEXTURE_2D, patch.texture); // 邦定紋理
for (u=1;u<=divs;u++) {
py = ((float)u)/((float)divs); // 計(jì)算v方向上的細(xì)分點(diǎn)的參數(shù)
pyold = ((float)u-1.0f)/((float)divs); // 上一個(gè)v方向上的細(xì)分點(diǎn)的參數(shù)
temp[0] = Bernstein(py, patch.anchors[0]); // 計(jì)算每個(gè)細(xì)分點(diǎn)v方向上貝塞爾曲面的控制點(diǎn)
temp[1] = Bernstein(py, patch.anchors[1]);
temp[2] = Bernstein(py, patch.anchors[2]);
temp[3] = Bernstein(py, patch.anchors[3]);
glBegin(GL_TRIANGLE_STRIP); // 開(kāi)始繪制三角形帶
for (v=0;v<=divs;v++) {
px = ((float)v)/((float)divs); // 沿著u軸方向順序繪制
glTexCoord2f(pyold, px); // 設(shè)置紋理坐標(biāo)
glVertex3d(last[v].x, last[v].y, last[v].z); // 繪制一個(gè)頂點(diǎn)
last[v] = Bernstein(px, temp); // 創(chuàng)建下一個(gè)頂點(diǎn)
glTexCoord2f(py, px); // 設(shè)置紋理
glVertex3d(last[v].x, last[v].y, last[v].z); // 繪制新的頂點(diǎn)
}
glEnd(); // 結(jié)束三角形帶的繪制
}
glEndList(); // 顯示列表繪制結(jié)束
free(last); // 釋放分配的內(nèi)存
return drawlist; // 返回創(chuàng)建的顯示列表
}
這里我們調(diào)用一個(gè)我認(rèn)為有一些很酷的值的矩陣。
void initBezier(void) {
mybezier.anchors[0][0] = makePoint(-0.75, -0.75, -0.50); // 設(shè)置貝塞爾曲面的控制點(diǎn)
mybezier.anchors[0][1] = makePoint(-0.25, -0.75, 0.00);
mybezier.anchors[0][2] = makePoint( 0.25, -0.75, 0.00);
mybezier.anchors[0][3] = makePoint( 0.75, -0.75, -0.50);
mybezier.anchors[1][0] = makePoint(-0.75, -0.25, -0.75);
mybezier.anchors[1][1] = makePoint(-0.25, -0.25, 0.50);
mybezier.anchors[1][2] = makePoint( 0.25, -0.25, 0.50);
mybezier.anchors[1][3] = makePoint( 0.75, -0.25, -0.75);
mybezier.anchors[2][0] = makePoint(-0.75, 0.25, 0.00);
mybezier.anchors[2][1] = makePoint(-0.25, 0.25, -0.50);
mybezier.anchors[2][2] = makePoint( 0.25, 0.25, -0.50);
mybezier.anchors[2][3] = makePoint( 0.75, 0.25, 0.00);
mybezier.anchors[3][0] = makePoint(-0.75, 0.75, -0.50);
mybezier.anchors[3][1] = makePoint(-0.25, 0.75, -1.00);
mybezier.anchors[3][2] = makePoint( 0.25, 0.75, -1.00);
mybezier.anchors[3][3] = makePoint( 0.75, 0.75, -0.50);
mybezier.dlBPatch = NULL; // 默認(rèn)的顯示列表為0
}
這是一個(gè)優(yōu)化的調(diào)位圖的函數(shù)。可以很簡(jiǎn)單的把他們放進(jìn)一個(gè)簡(jiǎn)單循環(huán)里調(diào)一組。
// 加載一個(gè)*.bmp文件,并轉(zhuǎn)化為紋理
BOOL LoadGLTexture(GLuint *texPntr, char* name)
{
BOOL success = FALSE;
AUX_RGBImageRec *TextureImage = NULL;
glGenTextures(1, texPntr); // 生成紋理1
FILE* test=NULL;
TextureImage = NULL;
test = fopen(name, "r");
if (test != NULL) {
fclose(test);
TextureImage = auxDIBImageLoad(name);
}
if (TextureImage != NULL) {
success = TRUE;
// 邦定紋理
glBindTexture(GL_TEXTURE_2D, *texPntr);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage->sizeX, TextureImage->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage->data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
}
if (TextureImage->data)
free(TextureImage->data);
return success;
}
僅僅加了曲面初始化在這。你每次建一個(gè)曲面時(shí)都會(huì)用這個(gè)。再一次,這里是一個(gè)用C++的好地方(貝塞爾曲面類?)。
int InitGL(GLvoid) // 初始化OpenGL
{
glEnable(GL_TEXTURE_2D); // 使用2D紋理
glShadeModel(GL_SMOOTH); // 使用平滑著色
glClearColor(0.05f, 0.05f, 0.05f, 0.5f); // 設(shè)置黑色背景
glClearDepth(1.0f); // 設(shè)置深度緩存
glEnable(GL_DEPTH_TEST); // 使用深度緩存
glDepthFunc(GL_LEQUAL); // 設(shè)置深度方程
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
initBezier(); // 初始化貝塞爾曲面
LoadGLTexture(&(mybezier.texture), "./Data/NeHe.bmp"); // 載入紋理
mybezier.dlBPatch = genBezier(mybezier, divs); // 創(chuàng)建顯示列表
return TRUE; // 初始化成功
}
首先調(diào)貝塞爾display list。再(如果邊線要畫)畫連接控制點(diǎn)的線。你可以用SPACE鍵開(kāi)關(guān)這個(gè)。
int DrawGLScene(GLvoid) { // 繪制場(chǎng)景
int i, j;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glTranslatef(0.0f,0.0f,-4.0f); // 移入屏幕4個(gè)單位
glRotatef(-75.0f,1.0f,0.0f,0.0f);
glRotatef(rotz,0.0f,0.0f,1.0f); // 旋轉(zhuǎn)一定的角度
glCallList(mybezier.dlBPatch); // 調(diào)用顯示列表,繪制貝塞爾曲面
if (showCPoints) { // 是否繪制控制點(diǎn)
glDisable(GL_TEXTURE_2D);
glColor3f(1.0f,0.0f,0.0f);
for(i=0;i<4;i++) { // 繪制水平線
glBegin(GL_LINE_STRIP);
for(j=0;j<4;j++)
glVertex3d(mybezier.anchors[i][j].x, mybezier.anchors[i][j].y, mybezier.anchors[i][j].z);
glEnd();
}
for(i=0;i<4;i++) { // 繪制垂直線
glBegin(GL_LINE_STRIP);
for(j=0;j<4;j++)
glVertex3d(mybezier.anchors[j][i].x, mybezier.anchors[j][i].y, mybezier.anchors[j][i].z);
glEnd();
}
glColor3f(1.0f,1.0f,1.0f);
glEnable(GL_TEXTURE_2D);
}
return TRUE; // 成功返回
}
KillGLWindow()函數(shù)沒(méi)有改動(dòng)
CreateGLWindow()函數(shù)沒(méi)有改動(dòng)
我在這里加了旋轉(zhuǎn)曲面的代碼,增加/降低分辨率,顯示與否控制點(diǎn)連線。
if (keys[VK_LEFT]) rotz -= 0.8f; // 按左鍵,向左旋轉(zhuǎn)
if (keys[VK_RIGHT]) rotz += 0.8f; // 按右鍵,向右旋轉(zhuǎn)
if (keys[VK_UP]) { // 按上鍵,加大曲面的細(xì)分?jǐn)?shù)目
divs++;
mybezier.dlBPatch = genBezier(mybezier, divs); // 更新貝塞爾曲面的顯示列表
keys[VK_UP] = FALSE;
}
if (keys[VK_DOWN] && divs > 1) { // 按下鍵,減少曲面的細(xì)分?jǐn)?shù)目
divs--;
mybezier.dlBPatch = genBezier(mybezier, divs); // 更新貝塞爾曲面的顯示列表
keys[VK_DOWN] = FALSE;
}
if (keys[VK_SPACE]) { // 按空格切換控制點(diǎn)的可見(jiàn)性
showCPoints = !showCPoints;
keys[VK_SPACE] = FALSE;
}
恩,我希望這個(gè)教程讓你了然于心而且你現(xiàn)在象我一樣喜歡上了貝塞爾曲面。;-)如果你喜歡這個(gè)教程我會(huì)繼續(xù)寫一篇關(guān)于NURBS的如果有人喜歡。請(qǐng)EMAIL我讓我知道你怎么想這篇教程。

