och雪花一分形問題。
前面講了無窮極數的概念,無窮極數是分型學的理論基礎。講到分型問題,我來給大家介紹一片漂亮的雪花叫Koch雪花,它是1904年由瑞典數學家柯赫構造的。下面一起來看一下這片Koch雪花是怎么構造而成的。
有的同學說這不是一個正三角形嗎?對,在正三角形的基礎上每條邊進行三等分,然后借用中間的邊構造一個新的小正三角形,然后把中間的邊抽掉。大家看一下這樣的形狀是不是更像一朵雪花了?但還不是。
繼續重復剛才的工作,每條邊三等分,借用中間的邊構造一個更小的正三角形,然后把中間邊抽掉,這個過程其實就是第二次分型。依次延續剛才的過程,每次都是三等分,每次都是用中間這條邊進行畫正三角形的過程,然后把中間小邊抽掉。大家看這就是三次分型的過程,依次類推進行無窮次的分型。
最終就可以得到一片非常美麗的雪花,它叫做Koch雪花。下面的問題是什么?請你用無窮極數所學的知識來判斷這片美麗的雪花的周長是多少?面積又是多少?
形結構最大程度地利用了細胞表面積來運輸能量
導語
Geoffrey West 等人1999年在Science 發表了一篇文章,用分形幾何來解釋生物異速標度律,引申出了生命的“第四維度”。集智俱樂部“冪律與規模”讀書會上,對生命3/4標度現象如何起源、4是否意味著生命有第四維度、如何從分形的角度理解維度等問題,做了大量探討,本文是對“生命第四維”問題探討后的一篇筆記。
該論文題目:
The Fourth Dimension of Life: Fractal Geometry and Allometric Scaling of Organisms
http://biology.unm.edu/jhbrown/Documents/Publications/WestBrown&Enquist1999S.pdf
自然選擇使生命的代謝能力最大化,最大限度地擴大了運輸資源和能量的表面積。例如我們的代謝能量會通過毛細血管的表面積運向全身上下細胞,從而促進細胞生長,維持生命所需的能量。
舉個很直觀的例子,盡管你的肺只有一個足球那么大,體積為5~6升,但是,血液中負責氧氣和二氧化碳交換的肺泡總表面積,幾乎有一個網球場那么大;而所有氣流通路的總長度幾乎是從倫敦到莫斯科的距離,約2500千米!
這是怎么做到的呢?
Peano曲線和大腸表面的分形褶皺
答案是分形。分形是20世紀系統科學提出的一個重要概念,它可以對d維的幾何體施加最大化的褶皺和扭曲而得到d + 1維的幾何體。例如褶皺的大腦皮層,以及扭曲纏繞的大腸。生命體內正是充滿了神奇的分形結構,才實現了空間填充的最大化。
即便在“不經意間”已經處處運用分形結構解決難題,但或許我們對其并沒有系統的認識。理解分形,從認識維度開始。
什么是維度
維度,又稱維數,在數學中被定義為獨立參數的數目,在物理學和哲學的領域內,是指獨立的時空坐標的數目。
0維是一個點,沒有長度;
1維是一條線,只有長度;
2維是一個面,由長度和寬度形成的面;
3維是一個體,在2維的基礎上加上高度所形成。
1維的線在其維度方向可以截出無窮多個0維的點;
2維的面從任意維度方向可以截出無窮多的1維的線;
3維的體可以截出無窮多的2維的面;
4維則是建立在3維之上又多一個維度,根據上面的概念的引申,4維的東西可以截出無窮多個“體”。
圖1 從0-4的維度示意圖 | wikipedia
在物理學上往往將時間作為第四維,與空間的3維一起構成4維時空,在任何時間來看(橫截觀察),就可以得到一個“體”。如果固定空間的一個維度,剩下的兩個維度和時間也可以合成一個體,可以形象地展示物體的運動情況,如圖2所示。
圖2太陽系在銀河系中的真實運動軌跡 | wikipedia
維度是這個形體上確定一個位置所需要的獨立坐標的個數。對于一個獨立的點來說,它沒有大小,確定點上的位置不需要任何坐標數據。但對于一條直線來說,就需要用坐標來確定線上點的位置。曲線也是如此。無論直線還是平面上的曲線還是空間上的連續曲線都只有一個維度。
平面和球面都是2維的,因為一個數據無法標定面上的某個具體位置,而是需要并且只需要2個數據。比如地球上的任何一個位置通過精度和維度就可以確定下來。
彎曲的三維形態難以想象,因為其要存在至少4維的空間中。
圖3 平面和空間中的曲線,也都是1維的
曲線是一維的,如圖3所示,這些曲線,無論是平面上的還是立體中的都是1維的,都可以通過一個數值來確定某個位置。
雖然一般人已經習慣了整數維,但有些時候維度不一定是整數,例如分形,維度可能會是一個非整的有理數或者無理數。
如何理解分形的維度
下面將介紹分形中最著名的幾個例子。
A.康托(Cantor)三分集
1883年,德國數學家康托(G.Cantor)提出了如今廣為人知的康托三分集。康托三分集的構造過程是:
把閉區間[0,1]平均分為三段,去掉中間的 1/3 部分段,則只剩下兩個閉區間[0,1/3]和[2/3,1]。
再將剩下的兩個閉區間各自平均分為三段,同樣去掉中間的區間段,這時剩下四段閉區間:[0,1/9],[2/9,1/3],[2/3,7/9]和[8/9,1]。
重復刪除每個小區間中間的 1/3 段。如此不斷的分割下去, 最后剩下的各個小區間段就構成了康托三分集。
康托三分集的維度為 D=log2 / log3=0.631
B.科赫雪花。
瑞典人Koch于1904年提出。給定線段AB,科赫曲線可以由以下步驟生成:
將線段分成三等份,形成3個線段
以中間段為底,向外(內外隨意)畫一個等邊三角形
將這個中間段移去
分別對目前的4個線段,重復1~3
科赫雪花維度為 D=log4 / log3=1.2618
C.門格海綿
1926年被奧地利數學家 Menger 首次描述。門格海綿的結構可以用以下方法形象化:
從一個正方體開始,把正方體的每一個面分成9個正方形。
再將把正方體分成27個小正方體,像魔方一樣。
把每一面的中間的正方體去掉,把最中心的正方體也去掉,留下20個正方體(第二個圖像)。
把每一個留下的小正方體都重復前面的步驟。
把以上的步驟重復無窮多次以后,得到的圖形就是門格海綿。
門格海綿的維度為 D=log20 / log3=2.7268
再次強調,維度是這個形體上確定一個位置所需要的獨立坐標的個數。
確定康托集上的某一個位置不需要整個實數域,因為有很多點已經挖去了。所以可以認為康托集要比一個點多,但不如一個曲線多。維度應該在0-1之間。維度應該在0-1之間。
而科赫雪花是在二維平面內,但是沒有覆蓋整個平面,確定上面的位置一個實數是不夠的,例如給定橫軸方向的實數而不指定縱軸位置的話無法確定集合上的一個元素,但縱軸并不是連續的,只有某些位置上才會有意義,所以應該大體上維度介于1-2之間。
而門格海綿用不著3個獨立的變量來確定一個位置,所以維度應該在2-3之間。從這個意義上講,能表示在二維平面的分形的維數不能超過2,在立體空間表示出來的分形不會超過3。
所以,West在生命第四維的文章中說V(和體積和質量有關的東西)是4維的,很容易受到的質疑。
嚴格的分形維度如何定義
嚴格的分形維度的定義為 Hausdorff 維數,可以通過覆蓋來進行解釋。
Hausdorff 提出:假設考慮的物體或圖形是歐氏空間的有界集合 ,用半徑為
的球覆蓋其集合時 , 假定是球的個數的最小值,則有分形維數。
我們先說一個長度為L的直線段,如果我們用更小的球去覆蓋,則需要的個數會隨著球的半徑減少而增加,
所以分形維數:
同理很容易說明平面是2維以及立體是3維的。
再來看康托集。假設整個長度為1,用最長的長度為1的直線段即可覆蓋,但如果用長度為1/3的線段的話,就不用3個了,兩個即可,因為中間是空的。繼續進行,若長度為1/9的線段的話,只需要4個線段。
所以當線段的長度變為上一個覆蓋的1/3的時候,需要的個數只增加到兩倍。
所以分形維數:。
同樣,對于科赫雪花,當使用長度為原來1/3的線段進行覆蓋時候,覆蓋物的個數會增加到4倍,
所以分形維數:。
生命第四維是一種類比
回到 West 關于生命第四維的文章,其目的是解釋生物基礎代謝率與生物量(質量)之間的非線性關系,更精確地說是3/4的冪律關系。也就是如果生物體體重增加一萬倍(10的四次冪),代謝率只增加到原來的一千倍(10的3次冪)。
West 在這個文章中運用Koch雪花那樣的“分形可以增加維度”的思想,將血管橫截面圓周分形化,從1維變為2維,將整個血管的表面積變為3維,并將其與基礎代謝率建立線性關系。而體積和質量相關部分為表面積的維度再增加一個維度變為4維。維度關系如下表所示。
表1 生物體血管的分形維度和傳統的歐氏幾何維度的關系
和體積有關的東西超過3維,是很難被接受的。集智學友在 West 訪華期間向其本人咨詢了這個問題, West 本人答復說,這個增加的維度并非實指。
生命的第四維度,在數學上看好像是的,但嚴格地說——“這是一個類比和拓展”。
參考文獻
https://zh.wikipedia.org/wiki/維度
http://v.youku.com/v_show/id_XNTYzNzQ4OTY0.html
https://baike.baidu.com/item/分形理論/1568038?fr=aladdin
https://www.cnblogs.com/WhyEngine/p/3998063.html
https://www.cnblogs.com/WhyEngine/p/3981674.html
https://baike.baidu.com/item/%E9%97%A8%E6%A0%BC%E6%B5%B7%E7%BB%B5/9005082?fr=aladdin
West G B, Brown J H, Enquist B J. The fourth dimension of life: fractal geometry and allometric scaling of organisms[J]. science, 1999, 284(5420): 1677-1679.
朱金兆, 朱清科. 分形維數計算方法研究進展[J]. 北京林業大學學報, 2002, 24(2): 71-78.
http://www.math.ubc.ca/~cass/courses/m308-03b/projects-03b/skinner/ex-dimension-koch_snowflake.htm
編輯:王怡藺
程 實 驗 報 告
實驗一:分形圖形繪制11
一、實驗目的11
二、實驗內容11
三、實驗心得1111
實驗二:三維場景繪制1212
一、實驗目的1212
二、實驗內容1212
三、實驗心得1919
1、實驗算法
繪制分形三角形算法思想:求出三角形ABC三條邊AB,BC,AC的中點坐標D,E,F,繪制三角形DEF,再對三角形ADF,BDE,CEF重復進行上述操作即可得到分形三角形。
繪制Koch雪花的算法思想:計算得到一條邊經過一次分形后的五個點的坐標,然后對分得的四條邊反復進行變換,對三角形的三邊進行上述變換即可得到Koch雪花。
繪制分形地毯圖形思想:計算得到中心正方形的周圍8個小正方形的左上頂點坐標與邊長,繪制8個小正方形,然后對8個小正方形分別進行上述操作即可得到分形地毯。
2、源程序
源程序如下:
#include <stdio.h>
#include <time.h>
#include <math.h>
#include <GL/glut.h>
#define ROUND(a) ((int )(a+0.5)) // 求某個數的四舍五入值
#define PI 3.141592654 // pi的預定義
int xf=100, yf=200; // 定義測試數據坐標x0,y0
int xl=500, yl=200; // 定義測試數據坐標x1,y1
int xt=xf + (xl - xf) / 2.0; // 畫等邊三角形時的第三個坐標x0
int yt=yf + (xl - xf) / 2.0 * tan(3.1415926 / 3.0);
// 畫等邊三角形時的第三個坐標x0
// 窗口初始化函數,初始化背景色,投影矩陣模式和投影方式
void init(void)
{
glClearColor(0.396, 0.655, 0.890, 0.0); //指定窗口的背景色為藍色
glMatrixMode(GL_PROJECTION); //對投影矩陣進行操作
gluOrtho2D(0.0, 600.0, 0.0, 600.0); //使用正投影
}
//繪制直線的函數
void lineDDA(GLint xa, GLint ya, GLint xb, GLint yb)
{
GLint dx=xb - xa, dy=yb - ya; //計算x,y方向的跨距
int steps, k; //定義繪制直線像素點的步數
float xIcre, yIcre, x=xa, y=ya; //定義步長的增量
//取X,Y方向跨距較大的值為步數
if (abs(dx) > abs(dy))
steps=abs(dx);
else
steps=abs(dy);
//根據步數來求步長增量
xIcre=dx / (float)steps;
yIcre=dy / (float)steps;
//從起點開始繪制像素點
for (k=0;k <=steps; k++)
{
glBegin(GL_POINTS);
glVertex2f(x, y);
glEnd();
x +=xIcre;
y +=yIcre;
}
}
// 中點Bresenham算法繪制直線
void MidBresenhamLine(GLint xa, GLint ya, GLint xb, GLint yb)
{
// 斜率k的四個狀態
// K01表示0<k<=1;
// KG1表示k>1;
// K_10表示-1<=k<0;
// KL_1表示k<-1
const int K01=0, KG1=1, K_10=2, KL_1=3;
int flag; // 標識斜率k的狀態
GLint dx, dy, d, upIncre, downIncre, x, y;
// 使b的橫坐標大于a的橫坐標
if (xa > xb)
{
x=xb; xb=xa; xa=x;
y=yb; yb=ya; ya=y;
}
if (yb >=ya && yb - ya < xb - xa)
flag=K01; // K01表示0<k<=1;
else if (yb - ya > xb - xa)
flag=KG1; // KG1表示k>1;
else if (yb <=ya && yb - ya > xa - xb)
flag=K_10; // K_10表示-1<=k<0;
else
flag=KL_1; // KL_1表示k<-1
x=xa;
y=ya;
dx=xb - xa; // 計算增量dx
dy=yb - ya; // 計算增量dy
// 當0<k<=1時
if (flag==K01)
{
d=dx - 2 * dy; // 計算d初值
upIncre=2 * dx - 2 * dy; // 計算步長增量
downIncre=-2 * dy;
}
// 當k>1時
if (flag==KG1)
{
d=2 * dx - dy; // 計算d初值
upIncre=2 * dx; // 計算步長增量
downIncre=2 * dx - 2 * dy;
}
// 當-1<=k<0時
if (flag==K_10)
{
d=-dx - 2 * dy; // 計算d初值
upIncre=-2 * dy; // 計算步長增量
downIncre=-2 * dx - 2 * dy;
}
// 當k<-1時
if (flag==KL_1)
{
d=-2 * dx - dy; // 計算d初值
upIncre=-2 * dx - 2 * dy; // 計算步長增量
downIncre=-2 * dx;
}
// 開始繪制直線
glBegin(GL_POINTS);
// 斜率為無窮大,即為一條豎直線時單獨考慮
if (dx==0)
{
// 使B的縱坐標更大
if (ya > yb)
{
y=yb; yb=ya; ya=y;
}
y=ya;
// 從A點向上繪制直線
while (y < yb)
{
glVertex2i(x, y);
y++;
}
}
else
{
// 橫向掃描,當掃描到B點橫坐標時退出
while (x <=xb)
{
glVertex2i(x, y);
// 如果直線斜率滿足(0,1]或[-1,0)時,X方向為最大位移方向且X增加
if (flag==K01 || flag==K_10)
x++;
// 如果直線斜率滿足(1, ∞)時,Y方向為最大位移方向且Y增加
if (flag==KG1)
y++;
// 如果直線斜率滿足(-∞, -1)時,Y方向為最大位移方向且Y遞減
if (flag==KL_1)
y--;
// 當判據d<0時進入
if (d < 0)
{
// 如果直線斜率滿足(0,1]時,y增加
if (flag==K01)
y++;
// 如果直線斜率滿足(-∞, -1)時,x增加
if (flag==KL_1)
x++;
// 更新判據d
d +=upIncre;
}
// d>0時
else
{
// 如果直線斜率滿足[-1,0)時,y遞減
if (flag==K_10)
y--;
// 如果直線斜率滿足(1, ∞)時,x增加
if (flag==KG1)
x++;
// 更新判據d
d +=downIncre;
}
}
}
glEnd(); // 結束繪制
}
// 遞歸繪制Koch分形圖形的一條邊,n為遞歸次數,inside為Koch圖形的朝向
// inside為1表示朝上或朝內,為0表示朝下或朝外
void Koch(float x0, float y0, float x1, float y1, int n, int inside)
{
// n>0時繼續遞歸
if (n > 0)
{
// A,B,C,D,E點為一條線上一次分形后的五個頂點
float xa, ya, xb, yb, xc, yc, xd, yd, xe, ye;
xa=x0; // A點的橫坐標與X0相等
ya=y0; // A點的縱坐標與Y0相等
xb=x0 + (x1 - x0) / 3.0; // B點為靠近(X0, Y0)的三等分點
yb=y0 + (y1 - y0) / 3.0;
// 如果朝向為上或內,C點的坐標值計算如下
if (inside)
{
xc=(x1 + x0) / 2.0 + (y0 - y1) * sqrt(3.0) / 6.0;
yc=(y1 + y0) / 2.0 + (x1 - x0) * sqrt(3.0) / 6.0;
}
// 如果朝向為下或外,C點的坐標值計算如下
else
{
xc=(x1 + x0) / 2.0 - (y0 - y1) * sqrt(3.0) / 6.0;
yc=(y1 + y0) / 2.0 - (x1 - x0) * sqrt(3.0) / 6.0;
}
// D點為靠近(X1, Y1)的三等分點
xd=x0 + 2 * (x1 - x0) / 3.0;
yd=y0 + 2 * (y1 - y0) / 3.0;
xe=x1; // E點的橫坐標等于X1
ye=y1; // E點的縱坐標等于Y1
Koch(xa, ya, xb, yb, n - 1, inside); // 對邊AB進行遞歸
Koch(xb, yb, xc, yc, n - 1, inside); // 對邊BC進行遞歸
Koch(xc, yc, xd, yd, n - 1, inside); // 對邊CD進行遞歸
Koch(xd, yd, xe, ye, n - 1, inside); // 對邊DE進行遞歸
}
// n=0時遞歸結束,繪制直線(x0, y0)到(x1, y1)
else
MidBresenhamLine(x0, y0, x1, y1);
}
// 遞歸繪制分形三角形,n為遞歸次數
void Triangle(float x0, float y0, float x1, float y1, float x2, float y2, int n)
{
// MID0為邊(x1, y1)與(x2, y2)中點
// MID1為邊(x0, y0)與(x2, y2)中點
// MID2為邊(x0, y0)與(x1, y1)中點
float midx0, midy0, midx1, midy1, midx2, midy2;
// n>0時進入遞歸
if (n > 0)
{
// 計算MID0的坐標
midx0=(x1 + x2) / 2.0;
midy0=(y1 + y2) / 2.0;
// 計算MID1的坐標
midx1=(x0 + x2) / 2.0;
midy1=(y0 + y2) / 2.0;
// 計算MID2的坐標
midx2=(x0 + x1) / 2.0;
midy2=(y0 + y1) / 2.0;
// 對(x0, y0),MID1,MID2組成的三角形遞歸
Triangle(x0, y0, midx1, midy1, midx2, midy2, n - 1);
// 對MID0,(x1, y1),MID2組成的三角形遞歸
Triangle(midx0, midy0, x1, y1, midx2, midy2, n - 1);
// 對MID0,MID1,(x2, y2)組成的三角形遞歸
Triangle(midx0, midy0, midx1, midy1, x2, y2, n - 1);
}
// n=0時開始繪制三角型的三條邊
else
{
MidBresenhamLine(x0, y0, x1, y1);
MidBresenhamLine(x1, y1, x2, y2);
MidBresenhamLine(x0, y0, x2, y2);
}
}
// 繪制內部填充的正方形,(x0, y0)為正方形左上方頂點,side為正方形邊長
void DrawSquare(double x0, double y0, double side)
{
glBegin(GL_LINES);
// 從y=y0掃描到y=y0-side,繪制從(x0, y)到(x0+side, y)的直線
for (int y=y0; y > y0 - side; y--)
{
glVertex2d(x0, y);
glVertex2d(x0 + side, y);
}
glEnd();
}
// 繪制分形地毯圖形,(x, y)為中心正方形的左上頂點,side為其邊長,n為遞歸次數
void DrawCarpet(double x, double y, double side, int n)
{
// 中心正方形周圍的8個小正方形的左上頂點坐標
double square0[2], square1[2], square2[2], square3[2],
square4[2], square5[2], square6[2], square7[2];
double len=side / 3.0; // 8個小正方形的邊長
DrawSquare(x, y, side); // 以(x, y)為左上頂點,side為邊長繪制實心正方形
// n>0時進入遞歸
if (n > 0)
{
// 計算第1個小正方形的左上頂點的坐標值
square0[0]=x - 2 * len;
square0[1]=y + 2 * len;
// 計算第2個小正方形的左上頂點的坐標值
square1[0]=x + len;
square1[1]=y + 2 * len;
// 計算第3個小正方形的左上頂點的坐標值
square2[0]=x + 4 * len;
square2[1]=y + 2 * len;
// 計算第4個小正方形的左上頂點的坐標值
square3[0]=x - 2 * len;
square3[1]=y - len;
// 計算第5個小正方形的左上頂點的坐標值
square4[0]=x + 4 * len;
square4[1]=y - len;
// 計算第6個小正方形的左上頂點的坐標值
square5[0]=x - 2 * len;
square5[1]=y - 4 * len;
// 計算第7個小正方形的左上頂點的坐標值
square6[0]=x + len;
square6[1]=y - 4 * len;
// 計算第8個小正方形的左上頂點的坐標值
square7[0]=x + 4 * len;
square7[1]=y - 4 * len;
// 對8個小正方形進行遞歸處理
DrawCarpet(square0[0], square0[1], len, n - 1);
DrawCarpet(square1[0], square1[1], len, n - 1);
DrawCarpet(square2[0], square2[1], len, n - 1);
DrawCarpet(square3[0], square3[1], len, n - 1);
DrawCarpet(square4[0], square4[1], len, n - 1);
DrawCarpet(square5[0], square5[1], len, n - 1);
DrawCarpet(square6[0], square6[1], len, n - 1);
DrawCarpet(square7[0], square7[1], len, n - 1);
}
}
// 繪制回調函數
void display()
{
glClear(GL_COLOR_BUFFER_BIT); // 設定顏色緩存中的值
glColor3f(1.0, 1.0, 1.0); // 設置直線顏色為白色
//lineDDA(50, 500, 300, 100); // 調用DDA算法函數繪制直線
//MidBresenhamLine(50, 500, 400, 200); // 調用中點Bresenham算法繪制直線
// 對三角形的三條邊分別調用Koch分形圖形繪制函數
//Koch(xf, yf, xl, yl, 4, 0);
//Koch(xf, yf, xt, yt, 4, 1);
//Koch(xt, yt, xl, yl, 4, 1);
//Triangle(xt, yt, xf, yf, xl, yl, 5); // 調用分形三角形繪制函數繪制圖形
DrawCarpet(200, 400, 200, 4); // 調用分形地毯繪制函數繪制圖形
glFlush(); //立即執行
}
// 主函數
int main(int argc, char ** argv)
{
glutInit(&argc, argv); // 初始化GLUT庫OpenGL窗口的顯示模式
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); // 初始化窗口的顯示模式
glutInitWindowSize(600, 600); // 設置窗口的尺寸
glutInitWindowPosition(100, 100); // 設置窗口的位置
glutCreateWindow("Hello OpenGL!"); // 創建窗口標題為“Hello OpenGL!”
init(); // 初始化
glutDisplayFunc(display); // 執行畫圖程序
glutMainLoop(); // 啟動主GLUT事件處理循環
}
3、實驗結果
圖1-1 中點Bresenham算法繪制直線結果
圖1-2 分型三角形繪制結果
圖1-3 Koch雪花繪制結果
圖1-4 分型地毯繪制結果
通過本次實驗,我對圖形學有了更深刻的認識,收獲很大。
第一次使用OpenGL編程,因為有了C語言的基礎,上手并不是特別困難,閱讀理解了模板之后,便能自己修改程序完成中點Bresenham算法,對直線的繪制算法有了更全面的認識,把書本上的知識通過代碼實現,并且能夠看到相當不錯的結果,這是很令人欣慰的;分形圖形的繪制,是對數學變換以及遞歸算法的應用,通過自己的觀察與計算,能夠繪制出較為復雜的圖形,分形三角形的繪制相對簡單,找出中點即可,Koch圖形的繪制需要找出變換后的五個頂點坐標,這需要進一步的計算才能實現,分形地毯的繪制是自己額外繪制的,也是不難實現的,分形圖形的繪制讓我體會到了數學與圖形學的美,對課程有了更加濃厚的興趣。
總的來說,完成本次實驗,我的整體能力得到了提高,對計算機圖形學這門課程的理解更加全面。
1、實驗算法
三維場景繪制:先繪制出基本的三維圖形,再通過簡單的平移、縮放、旋轉變換,得到一系列圖形的組合,最后加入光照、材質等屬性,設置合適的觀測視角,即可實現簡單三維場景的繪制。
本實驗繪制三維簡單城堡,對城堡的每個部分進行了分解,運用了OpenGL顯示列表,再對每個部分進行各種變換,最后繪制出完整的城堡模型。
2、源程序
源程序如下:
#include <stdio.h>
#include <GL/glut.h>
#include <math.h>
#define PI 3.1415265359 // 定義常量π
static GLfloat MatSpec[]={1.0, 1.0, 1.0, 1.0}; // 材料顏色
static GLfloat MatShininess[]={50.0}; // 光澤度
static GLfloat LightPos[]={-2.0, 1.5, 1.0, 0.0}; // 光照位置
static GLfloat ModelAmb[]={0.5, 0.5, 0.5, 0.0}; // 環境光顏色
// 需要畫出來的哨塔的數量和墻面的數量
GLint TowerListNum, WallsListNum;
GLint WallsList;
// 哨塔相關參數
int NumOfEdges=30; // 細分度(其值越高,繪制越精細)
GLfloat LowerHeight=3.0; // 哨塔上方倒圓臺的下底面高度
GLfloat HigherHeight=3.5; // 哨塔上方倒圓臺的上底面高度
GLfloat HR=1.3; // 哨塔上方倒圓臺的上底面半徑
// 城墻相關參數
GLfloat WallElementSize=0.2; // 城墻上方凸起部分的尺寸
GLfloat WallHeight=2.0; // 城墻上方凸起部分的高度
// 繪制城墻上方凸起部分
void DrawHigherWallPart(int NumElements)
{
glBegin(GL_QUADS); // 開始繪制四邊形
// NumElements繪制凸起部分的數目,i小于其值時進入循環
for (int i=0; i < NumElements; i++)
{
glNormal3f(0.0, 0.0, -1.0); // 設置法向量為Z軸負半軸方向
// 繪制四邊形的四個頂點
glVertex3f(i * 2.0 * WallElementSize, 0.0, 0.0);
glVertex3f(i * 2.0 * WallElementSize, WallElementSize, 0.0);
glVertex3f((i * 2.0 + 1.0) * WallElementSize, WallElementSize, 0.0);
glVertex3f((i * 2.0 + 1.0) * WallElementSize, 0.0, 0.0);
}
glEnd(); // 結束繪制
}
// 繪制城墻
void DrawWall(GLfloat Length)
{
glBegin(GL_QUADS); // 開始繪制四邊形
glNormal3f(0.0, 0.0, -1.0); // 設置法向量為Z軸負半軸方向
// 繪制四邊形的四個點
glVertex3f(0.0, 0.0, 0.0);
glVertex3f(0.0, WallHeight, 0.0);
glVertex3f(Length, WallHeight, 0.0);
glVertex3f(Length, 0.0, 0.0);
glEnd(); // 結束繪制
// i為當前繪制的城墻上的凸起的數目
int i=(int)(Length / WallElementSize / 2);
// 保證凸起的總長度小于城墻的長度
if (i * WallElementSize > Length)
i--; // 如果凸起的總長度大于城墻長度,凸起數目減一
glPushMatrix();
glTranslatef(0.0, WallHeight, 0.0); // 平移變換
DrawHigherWallPart(i); // 繪制城墻上方凸起
glPopMatrix();
}
// 初始化函數,繪制哨塔,城墻
void Init(void)
{
/* 繪制哨塔 */
TowerListNum=glGenLists(1); // 生成哨塔顯示列表
GLfloat x, z; // 繪制時的法向量x,z分量
GLfloat NVectY; // 哨塔上方部分倒圓臺的法向量y分量
glNewList(TowerListNum, GL_COMPILE); // 用于創建和替換一個顯示列表函數原型
glBegin(GL_QUADS); // 繪制四邊形
// 創建塔身的圓柱體部分
int i=0;
for (i=0; i < NumOfEdges - 1; i++)
{
// 計算前兩個點法向量的x,z坐標
x=cos((float)i / (float)NumOfEdges * PI * 2.0);
z=sin((float)i / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, 0.0, z); // 設置法向量方向
// 以上述法線方向繪制前兩個頂點
glVertex3f(x, LowerHeight, z);
glVertex3f(x, 0.0, z);
// 計算后兩個點法向量的x,z坐標
x=cos((float)(i + 1) / (float)NumOfEdges * PI * 2.0);
z=sin((float)(i + 1) / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, 0.0, z); // 設置法向量方向
// 以上述法線方向繪制后兩個頂點
glVertex3f(x, 0.0, z);
glVertex3f(x, LowerHeight, z);
}
// 計算最后一個四邊形的前兩個點法向量的x,z坐標
x=cos((float)i / (float)NumOfEdges * PI * 2.0);
z=sin((float)i / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, 0.0, z); // 設置法向量方向
// 以上述法線方向繪制前兩個頂點
glVertex3f(x, LowerHeight, z);
glVertex3f(x, 0.0, z);
// 計算最后一個四邊形的后兩個點法向量的x,z坐標
x=cos(1.0 / (float)NumOfEdges * PI * 2.0);
z=sin(1.0 / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, 0.0, z); // 設置法向量方向
// 以上述法線方向繪制后兩個頂點
glVertex3f(x, 0.0, z);
glVertex3f(x, LowerHeight, z);
// 哨塔下方圓柱體部分繪制完成
// 繪制哨塔上方倒圓臺部分
// 計算法向量NVect y分量的值
NVectY=(HR - 1.0) / (LowerHeight - HigherHeight) * (HR - 1.0);
for (i=0; i < NumOfEdges - 1; i++)
{
// 計算四邊形前兩個頂點的法向量x,z值
x=cos((float)i / (float)NumOfEdges * PI * 2.0);
z=sin((float)i / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, NVectY, z); // 設置法向量方向
// 繪制四邊形前兩個頂點
glVertex3f(x * HR, HigherHeight, z * HR);
glVertex3f(x, LowerHeight, z);
// 計算四邊形后兩個頂點的法向量x,z值
x=cos((float)(i + 1) / (float)NumOfEdges * PI * 2.0);
z=sin((float)(i + 1) / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, NVectY, z); // 設置法向量方向
// 繪制四邊形后兩個頂點
glVertex3f(x, LowerHeight, z);
glVertex3f(x*HR, HigherHeight, z*HR);
}
// 計算最后一個四邊形前兩個頂點的法向量x,z坐標
x=cos((float)i / (float)NumOfEdges * PI * 2.0);
z=sin((float)i / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, NVectY, z); // 設置法向量方向
// 繪制最后一個四邊形前兩個頂點
glVertex3f(x * HR, HigherHeight, z * HR);
glVertex3f(x, LowerHeight, z);
// 計算最后一個四邊形后兩個頂點的法向量x,z坐標
x=cos(1.0 / (float)NumOfEdges * PI * 2.0);
z=sin(1.0 / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, NVectY, z); // 設置法向量方向
// 繪制最后一個四邊形前兩個頂點
glVertex3f(x, LowerHeight, z);
glVertex3f(x*HR, HigherHeight, z*HR);
glEnd(); // 繪制結束
glEndList(); // 標志顯示列表的結束
/* 繪制哨塔 */
/* 繪制城墻和大門 */
WallsListNum=glGenLists(1); // 創建城墻顯示列表
glNewList(WallsListNum, GL_COMPILE); // 說明顯示列表的開始
DrawWall(10.0); // 調用城墻繪制函數繪制左側城墻
glPushMatrix(); // 變換矩陣壓棧
glTranslatef(10.0, 0.0, 0.0); // 平移變換
glPushMatrix(); // 變換矩陣壓棧
glRotatef(270.0, 0.0, 1.0, 0.0); // 旋轉變換
DrawWall(10.0); // 調用城墻繪制函數繪制后側城墻
glPopMatrix(); // 恢復變換矩陣
glTranslatef(0.0, 0.0, 10.0); // 平移變換
glPushMatrix(); // 變換矩陣壓棧
glRotatef(180.0, 0.0, 1.0, 0.0); // 旋轉變換
DrawWall(5.0); // 調用城墻繪制函數繪制右側后方城墻
glRotatef(90.0, 0.0, 1.0, 0.0); // 旋轉變換
glTranslatef(0.0, 0.0, 5.0); // 平移變換
DrawWall(5.0); // 調用城墻繪制函數繪制右側中間城墻
glPopMatrix(); // 恢復變換矩陣
glTranslatef(-5.0, 0.0, 5.0); // 平移變換
glPushMatrix(); // 變換矩陣壓棧
glRotatef(180.0, 0.0, 1.0, 0.0); // 旋轉變換
DrawWall(5.0); // 調用城墻繪制函數繪制最右側城墻
glPopMatrix(); // 恢復變換矩陣
glPushMatrix(); // 變換矩陣壓棧
glRotatef(90.0, 0.0, 1.0, 0.0); // 旋轉變換
glTranslatef(0.0, 0.0, -5.0); // 平移變換
DrawWall(6.0); // 調用城墻繪制函數繪制前方大門右邊城墻
// 繪制前方大門
glTranslatef(6.0, 0.0, 0.0); // 平移變換
glBegin(GL_QUADS); // 繪制四邊形
glNormal3f(0.0, 0.0, -1.0); // 設置法向量
// 繪制四邊形四個頂點
glVertex3f(0.0, WallHeight / 2.0, 0.0);
glVertex3f(0.0, WallHeight, 0.0);
glVertex3f(3.0, WallHeight, 0.0);
glVertex3f(3.0, WallHeight / 2.0, 0.0);
glEnd(); // 繪制結束
i=(int)(3.0 / WallElementSize / 2); // 計算大門上方凸起數目
if (i * WallElementSize > 3.0)
i--; // 如果凸起總長度大于大門長度,凸起數目減一
glPushMatrix(); // 變換矩陣壓棧
glTranslatef(0.0, WallHeight, 0.0); // 平移變換
DrawHigherWallPart(i); // 繪制大門上方凸起
glPopMatrix(); // 恢復變換矩陣
glTranslatef(3.0, 0.0, 0.0); // 平移變換
DrawWall(6.0); // 繪制前方大門左側城墻
glPopMatrix(); // 恢復變換矩陣
glPopMatrix(); // 恢復變換矩陣
glEndList(); // 標識顯示列表的結束
/* 繪制城墻和大門 */
}
// 繪制函數
void Display(void)
{
glClearColor(1, 1, 1, 0); // 設置背景色
// 以上面顏色清屏并清除深度緩存
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity(); // 初始化矩陣
glLightfv(GL_LIGHT0, GL_POSITION, LightPos); // 指定光源的位置
glTranslatef(-7.0, -4.0, -20.0); // 平移變換
glRotatef(85.0, 0.0, 1.0, 0.0); // 旋轉變換
glRotatef(15.0, 0.0, 0.0, 1.0); // 旋轉變換
/* 繪制地面 */
glBegin(GL_POLYGON); // 繪制多邊形
glNormal3f(0.0, 1.0, 0.0); // 設置法向量
// 繪制6個頂點
glVertex3f(0.0, 0.0, 0.0);
glVertex3f(10.0, 0.0, 0.0);
glVertex3f(10.0, 0.0, 10.0);
glVertex3f(5.0, 0.0, 15.0);
glVertex3f(0.0, 0.0, 15.0);
glVertex3f(0.0, 0.0, 0.0);
glEnd(); // 繪制結束
/* 繪制地面 */
// 設置全局環境光為雙面光
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);
// 執行城墻顯示列表
glCallList(WallsListNum);
// 取消設置全局環境光為雙面光
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE);
// 執行哨塔顯示列表繪制第一個哨塔
glCallList(TowerListNum);
glTranslatef(10.0, 0.0, 0.0); // 平移變換
// 執行哨塔顯示列表繪制第二個哨塔
glCallList(TowerListNum);
glTranslatef(0.0, 0.0, 10.0); // 平移變換
// 執行哨塔顯示列表繪制第三個哨塔
glCallList(TowerListNum);
glTranslatef(-5.0, 0.0, 5.0); // 平移變換
// 執行哨塔顯示列表繪制第四個哨塔
glCallList(TowerListNum);
glTranslatef(-5.0, 0.0, 0.0); // 平移變換
// 執行哨塔顯示列表繪制第五個哨塔
glCallList(TowerListNum);
glFlush();// 立即執行
glutSwapBuffers();// 交換緩沖區
}
// 窗口改變函數
void Reshape(int x, int y)
{
glViewport(0, 0, x, y); // 設置視口
glMatrixMode(GL_PROJECTION); // 指定當前操作投影矩陣堆棧
glLoadIdentity(); // 重置矩陣函數,將之前的變換消除
gluPerspective(40.0, (GLdouble)x / (GLdouble)y, 1.0, 200.0); // 投影變換
glMatrixMode(GL_MODELVIEW); // 指定當前操作視景矩陣堆棧
}
// 主函數
int main(int argc, char **argv)
{
glutInit(&argc, argv); // 初始化
// 設置窗口初始顯示模式
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(1200, 600); // 顯示框大小
glutInitWindowPosition(100, 100); // 確定顯示框左上角的位置
glutCreateWindow("Castle"); // 窗口名字
glEnable(GL_DEPTH_TEST); // 打開深度檢測
// 設置正反面都為填充方式
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glutDisplayFunc(Display); // 調用繪圖函數
glutReshapeFunc(Reshape); // 調用窗口改變函數
// 定義鏡面材料顏色
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, MatSpec);
// 定義材料的光澤度
glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, MatShininess);
glEnable(GL_LIGHTING); // 啟用光源
glEnable(GL_LIGHT0); // 使用指定燈光
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ModelAmb); // 設定全局環境光
Init(); // 調用初始化函數
glutMainLoop(); // 啟動主GLUT事件處理循環
return 0;
}
3、實驗結果
三維場景繪制結果如圖2-1、2-2所示:
圖2-1 三維場景繪制結果A
圖2-2 三維場景繪制結果B
通過第二次實驗,我對三維場景繪制的認識更加全面,對OpenGL的運用也更加熟練。
三維場景的繪制,相比第一次實驗較為復雜,對OpenGL的熟練使用要求更多,需要對OpenGL中的平移、縮放和旋轉變換有足夠的了解,在繪圖過程中,運用了OpenGL的顯示列表繪制,大大降低了代碼的復雜度,繪制簡單三維城堡的的過程中,先是繪制城堡的一小部分,如哨塔的上下兩個部分,城墻上面的凸起等,在經過一系列變換,繪制出所有的哨塔,城墻以及城墻上方的凸起等等,繪制過程中,對法向量的計算尤為重要,因為繪制圓柱體和圓臺時采用的是繪制四邊形合成的方法,所以在繪制每個四邊形時法向量的設置需要格外注意,對每個部分的定位也要準確,每一步的繪制都需要考慮到下一步的變換,所以在對繪制過程中的坐標變換時也需要注意。
綜上,兩次實驗的完成,讓我一點一滴的開始OpenGL編程,其中的收獲是不言而喻的,對課程的學習也起到了重要作用。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。