隨機分形地形生成

a.最近在學習OpenGL的東西時,無意發現了一篇關於”分形“的文章。”分形“由於以前接觸過一點,記得和”過程內容生成“有莫大的關係,它強大而神奇的功能一直讓我很好奇。看了原文作者的這篇文章後,我斷定這是一篇學習”分形“的入門級別的好讀物。 雖然文章並沒有過多解釋關於”分形“的數學知識,但通過一個經典算法的學習,會讓人對它的應用更有”通透“的理解。因此我決定將它譯成中文,供兄弟們一起學習。

b.調子拔得有點高了,實際上這篇文章寫於1997年,距離現在已經過去了12年的歲月。因此它是一個不折不扣的老古董。對此我有兩個感觸:一是國外怎麼這麼早就有人在研究這玩意,而直到現今我對分形纔剛瞭解沒多久,這當中的差距差了多少光年?二是我會不會有點傻x,有人會翻12年前的東西麼?不過感覺對我個人的成長還是很值的。原文由Paul Martz所寫,在此表示感謝,儘管他本人無論如何也不會知道。

c.這篇文章翻了大概有一個禮拜還多的時間(3月9日——3月18日)。當然和能力和效率有關了,這期間也還要處理很多很複雜的人生問題,包含有:爲怎麼找工作發愁,爲怎麼考研發愁,爲怎麼完成畢設發愁,和女友吵架N多,看《不能承受的生命之輕》,看押井守的《攻殼機動隊》3部和《天空殺手》,看楊德昌的《一一》(全是小明推薦來的,呵呵,兄弟啊),和弟兄們交流這些糟心事,一起抽菸喝悶酒。這裏權當日記小結一下,不打算闡述更加深刻的思考。

d.還不得不說下博客發佈編輯器。我花了一週時間翻文章並不冤枉,可是當我把完成的東西從word裏轉帖到這裏時,卻花了一上午的時間,因爲格式轉換的問題,不得不手動去調,真是鬱悶又冤枉。期間還用了SCribeFire,可也沒啥用。現在word和在線發佈真的無法無縫轉換麼,jesus christ 

e.因此,發佈成在線的html頗費體力,效果也不好,沒辦法大家湊合看吧。對於習慣於看雙語的朋友我提供了pdf版本下載(點擊這裏下載)。另外我給出原文地址鏈接: http://gameprogrammer.com/fractal.html 。 想看E文的直接去看。

f.對於翻譯,有些詞語是我擅自理解翻的,也許保留它們的原文更好(相應在文中以後綴(斜體)給出)。另外有些不太容易理解的關鍵信息也保留了原文,但在這裏不會列出。詞語如下:

Generating Random Fractal Terrain   隨機分形地形生成
The Definition and Rendering of Terrain Maps  地形圖的定義和渲染 
diamond-square algorithm  “菱形-正方形” 算法 (開始我以爲是什麼“鑽石-廣場”算法,搞得一頭霧水....-_-!)
tessellate 鑲嵌
Mandelbrot set  曼德勃羅特集
Midpoint Displacement in One Dimension  一維“中點替換”算法
fractal image compression  分形圖像壓縮
Chaos and Fractals, New Frontiers of Science  《混沌和分形,科學的新前沿》(網上找了找這方面的資料,沒發現有中譯本,因此尚不知該書的準確譯名是什麼)

g.另外是我lookof自己,譯者,擅自補充了一些話來讓語句變得更通順或更易理解的,或者是對此作出的一些解釋。不得不說明原文並沒有這麼寫,而且興許我的理解有問題。不過我所作的補充大多都是無關緊要的環節,不會影響大家理解關鍵的算法思想。對於是關鍵環節而拿捏不準的地方,我都提供了原文。補充以後綴(下劃線)給出。(注:這可不是超鏈接..... -_-!) 

如: 查閱本文末尾的參考書目(對學習分形知識而言)是一個好的開始。

h.參考書目不譯。

 

 


隨機分形地形生成




---------------------------------------

目錄:


第一部分:隨機分形地形生成

  • 引言
  • 自相似“一維中點替換”算法
  • 高度圖
  • “菱形-正方形”算法
  • 天空雲圖
  • 其他方法


第二部分:關於示例程序及其源代碼

  • 安裝
  • 使用示例程序
  • 代碼結構


參考書目


---------------------------------------






第一部分:隨機分形地形生成(Generating Random Fractal Terrain)


引言


    10年前,我偶然發現了SIGGRAPH小組於1986年的會議記錄,對其中的一篇文章感到特別得肅然起敬。這篇文章的題目是《地形圖的定義和渲染》(Definition and Rendering of Terrain Maps),作者是Gavin S.P.Miller1 。該文描述了很多分形地形生成的算法,而且作者也介紹了一種新的算法。小組其他成員認爲,這種新算法較之以前是一種改進。

    起初,我對這些算法的印象非常深刻(儘管作者認爲這些算法依然有“瑕疵”),它們竟然能創造出如此令人難以置信的風景圖片!然而接下來隨着深入文章,我被這些精巧絕妙的算法“驚呆”了。

    自那以後我就對分形地形上癮了。

    隱藏在該算法背後的數學知識可謂相當複雜。儘管如此(幸運的是),能不能完全弄清楚這些數學理論對學習該算法並無影響。這很好。因爲假如我在解釋算法之前,不得不先解釋所有這些數學的話,那我們這輩子也接觸不到這個算法了。除此之外,涉及到分形的數學概念光從字面上統計就有成噸的那麼多。查閱本文末尾的參考書目(對學習分形知識而言)是一個好的開始。

    出於同樣的原因,我不會深入到數學細節裏,我無法在這裏包含一個分形理論的概觀,也無法羅列出每一件它們能夠做的事情。取而代之的是,我將描述隱藏在“分形地形生成”背後的概念,然後着重給出一個我個人最喜歡的算法:“菱形-正方形”算法(diamond-square algorithm) 。我會告訴大家如何利用這個算法來靜態地“鑲嵌”(tessellate)一組高度數據,這組高度數據可以用來生成幾何地形數據,地形紋理貼圖,以及雲團紋理貼圖。

    利用分形地形你可以做什麼?我假設你已經知道了答案,因爲這就是你爲什麼會讀本文的原因。隨機地形圖對於飛行模擬器,以及製作一塊紋理貼圖來當做背景(比如顯示一座遙遠的山脈)來說,具有非常好的效果。同樣的算法也可以用來爲天空雲團生成紋理貼圖。

    在我繼續講下去前,發佈一條免責聲明:我不是一個遊戲程序員。如果你是因爲想要一個更快的渲染地形的算法而讀此文的話,你來錯了地方。我只是介紹生成這種地形模型的過程。至於你如何渲染,取決於你自己。


自相似


    藏在任何“分形”背後的一個關鍵概念是:自相似。當一個物體放大它自己後,它的局部子集相似於(或者相同於)整體和周圍的局部子集。2 

    想一想人體的循環系統,這就是一個自然界中“自相似”的好例子。從最大的動脈和靜脈開始,一路直下到最小的毛細血管,都是一模一樣的枝杈結構。如果你不知道你是在用顯微鏡觀察它們的話,你就會分不清哪個是毛細血管,哪個是動脈。

    現在考慮一個簡單的球體。它是自相似的嗎?不是。 非常顯著地放大很多倍以後,它就不再像個球了,而是像一個平面。如果你不相信我,只要看看外面好了。除非你閱讀本文的時候正好位於外太空的軌道上(汗。。。太幽默了),否則你將看不出任何關於“地球是圓的”這一跡象。 球體是非自相似的,描述它的最佳途徑是傳統的歐幾里德幾何,而非分形幾何。

    地形算是“自相似”這一範疇。你手中握着的岩石斷面的鋸齒邊有着相同的不規律性,就像遙遠地平線的一條山脊一樣。(因此地形的這個特點)就允許我們利用分形思想來產生仍然像地形的地形,而不用考慮地形呈現時的縮放比例。

    一條關於“自相似”的旁註:最嚴格的意義,它指的是“自相同”,就是說,無論放大或是縮小多少倍,它自己的精確的微觀拷貝版本都是可見的。(that is, exact miniature copies of itself are visible at increasingly small or large scales.)(我自己的理解是,它的局部是嚴格相同於整體的,即局部和整體一模一樣,並且可以無窮無盡細分下去)。實際上我不知道自然界中是否存在“自相同”的分形現象。但是曼德勃羅特集(Mandelbrot set)是自相同的。我不能再深入去講曼德勃羅特集了,更多信息請查閱參考書目。
    

“一維中點替換”算法(Midpoint Displacement in One Dimension )


    我稍後會講到的“菱形-正方形”算法,使用了一種二維“中點替換”算法。爲了幫助你掌握它,我們首先看看在一維下的“中點替換”算法。

    一維“中點”替換算法是在遙遠的地平線上畫一條山脊線的極好算法。它是這樣工作的:

以一條水平線段開始.
重複很多次{
  對場景中的每一條線段{
   找到線段的中點.
   在y軸上用一個隨機值替換中點值.
   縮小隨機值的取值範圍.
  }
}

    你縮減隨機值取值範圍的幅度是多少?這取決於你想要的效果有多“粗糙”。每一輪你縮減的越多,最後的山脊線就會越“平滑”。如果你基本不縮減,最後的山脊線就會像鋸齒一樣參差不齊。這表明你可以把“粗糙度”設定爲一個常數,我呆會兒會解釋怎麼做。

    我們來看一個例子。現在我們有一條線段,X軸上它在區間[-1.0,1.0]裏,Y軸上每一處都是0 。 開始的時候,我們設定一個隨機值的取值範圍[-1.0, 1.0](可以隨意設)。 所以我們將在這個範圍裏產生一個隨機值,然後用這個隨機值替換中點值。完成這步後,我們有:

 


    現在開始第二輪循環。我們這時有兩端線段,每一段都是原始線段的一半。我們讓隨機值取值範圍也減半,所以現在變成[-0.5,0.5] 。我們爲兩個中點值各自在這個範圍裏生成一個隨機值以之替換。現在的結果是:


 

    我們再次縮減隨機值取值範圍,因此現在它變成了[-0.25,0.25] 。在這個範圍裏再生成四個隨機值,用來替換現在的四個中點值,完成後我們有:

 

 

    你應該注意兩點。

    首先,它是遞歸的。而事實上,它也可非常自然地用迭代手法來實現。因此在(一維畫線)這個例子裏,無論是遞歸還是迭代都可行。但是對於生成“表面(即地形)”的代碼而言,用迭代實現比用遞歸實現有更多的好處。因此爲了(畫“線”和畫“面”)一致起見,給出的示例程序中,無論畫“線”還是畫“面”,都是用迭代方法實現的。

    其次,這是一個非常簡單的算法,然而,它卻創造處一個非常複雜的結果。這就是分形算法的魅力所在。幾條非常簡單的指令就能創造出一個非常豐富而且細膩的圖像。

    這裏我扯句題外話:只要用一組精巧的指令集就能創造出一幅複雜圖像的實現,引領起一個新技術領域的研究,被稱爲“分形圖像壓縮( fractal image compression)”技術。該技術的思想是,存儲用來創造圖形的簡單而又遞歸的指令,而不是存儲圖像本身。這種技術對於創造出自然界中真正有“分形”現象的圖像而言,效果是極其好的,因爲指令比圖像數據佔據的空間要小得多。《混沌和分形,科學的新前沿》3 (Chaos and Fractals, New Frontiers of Science 3 ) 有一個章節和一個附錄是專門講這個話題的,一般來講對任何一個“分形迷”而言,都值得一讀。

    言歸正傳。

    不必費多少勁兒,你就可以把這個(一維“中點”替換)函數的輸出結果讀入到一個繪圖程序裏,來生成點類似這樣的東西:

 

 

      這種圖像可以用來,打個比方說,作爲一幅窗外的風景。於此一件不錯的事情是,它是環繞的(即把兩個邊對接起來是契合的),這樣你就可以用一張相對較小的圖片來包裹整個場景。當然,如果你不介意無論從哪個方向看,都會看到同一座山的話。

    接下來,在我們進入“二維分形表面”這一話題之前,你需要了解下什麼是“粗糙度常數”。這是一個決定每次循環時隨機值取值範圍應當縮減多少的值,因而這個值會決定最終結果的粗糙程度。示例程序中我們使用了一個在區間[0.0,1.0]之間的浮點型數,我們稱它爲H 。那麼2^(-H)  就是一個在區間[0.5,1.0]之間的值。隨機值取值範圍在每一輪循環中都會乘以這個值(指的是2^(-H)。當H設置爲1時,隨機值取值範圍每輪循環都會減半,得到的結果是一個非常光滑的不規則形狀;當H設置爲0時,取值範圍則根本不會縮減,得到的結果將是非常參差不齊的鋸齒狀形狀。(這裏關於H的理解有點搞頭,不過經過本人詳細推敲,這段話是沒有問題的。H無疑是指粗糙度常數,但隨機值取值範圍每次乘的數是2^(-H) 。比如就像上面說的,如果你定的這個常數爲1,那麼就會使隨機值取值範圍每次都乘1/2,即第一次在[0,1]內取一個隨機值,第二次在[0,0.5]內取,第三次在[0,0.25]內取。這樣看來,H是通過控制2^(-H) 來間接控制隨機值取值範圍的。如果兄弟們還有不明白的可來信詢問。:-))

    以下是隨着H值的變化,所渲染出來的不同的山脊線:

 

 

高度圖


    上面描述的“中點替換”算法使用了一個表徵高度值的一維數組來實現。這個高度值指的是每條線段端點的垂直位置。該數組實際上就是一維的高度圖。它反應的是X軸索引到高度值Y的映射。

    爲了模擬隨機地形,我們欲把上面那個算法(指“中點替換”算法)推廣到三維空間,而且爲了達到這個目的,我們需要一個表徵高度的二維數組。這個數組把X軸和Z軸的聯合索引映射到表徵高度的Y軸上去(就是說Y是X與Z的多元函數,高數裏的內容)。注意,雖然我們的最終目標是要產生一個三維座標信息,但這裏這個二維數組僅僅存儲Y軸上的值,X軸和Z軸的值我們可以在解析數組的時候即時動態地生成。

    通過爲每一個高度值分配一種顏色,你可以把一幅高度圖顯示成一幅圖片。下面的這幅高度圖,用白色表示地形中高的地方(Y值大),用黑色表示地形中低的地方(Y值小): 

 

 

    用這種方式渲染一幅高度圖對生成“雲團紋理貼圖”也是有效的,這個呆會兒我會解釋。同樣,這種方式也可以用來隨機生成一幅高度圖(個人認爲,這纔是這篇文章的精髓。因爲後面介紹的算法就是在解決“隨機生成一幅高度圖”這個問題的)。

    現在,我來介紹如何生成“存儲高度圖信息”的二維數組。(即如何隨機生成一幅高度圖。)


“菱形-正方形”算法


    正如我在本文開始的時候提到的,我是在Gavin S. P. Miller 的文章中第一次知道了“隨機地形生成”這個概念。具有諷刺意義的是,在文中,Miller把“菱形-正方形”算法看作一種存在“缺陷”的算法,之後他就開始介紹另一種基於“加權平均和控制點”的算法了。(and he then goes on to describe a different algorithm based on weighted averaging and control points. )

(下面一段話,考慮到自己翻功拙劣,於是原文附上。因爲我並不能太好地表達他的意思。好在這段是評論Miller對該算法的看法的,不是理解算法的關鍵。)

    Miller對“菱形-正方形”算法的抱怨根源於他嘗試利用該算法來生成一座山。(山會坐落在X軸和Z軸所代表的平面網格中)。他要求平面網格的中心位置的高度(也就是峯值)是人工賦值的,而其它所有點的高度都是隨機產生的。如果Miller當初僅僅讓中心點的高度也同樣隨機產生,那麼連他也會不得不承認,該算法就像一個地形生成器一樣工作得那麼優雅。“菱形-正方形”算法可以用來生成一座帶有一個峯頂的山,這是通過對數組“種入”隨機數來辦到的。當然不僅僅是隻對數組的中心點這一個點植入一個種子數來達到可接受的結果。(呃,太繞了,就是說其它的點也需如此吧。建議大家看原句,我好菜。)他也對其它固有的摺痕問題發了點牢騷。但是(不管怎樣),你該有自己的判斷。這個算法最初由Fournier, Fussell, 和Carpenter 所描述4

(Miller's complaints with the diamond-square algorithm stem from his attempt to force the algorithm into creating a mountain, that is, with a peak, by artificially increasing the height of the grid center-point. He lets all other points in the array generate randomly. If Miller had simply generated the center-point randomly, then even he would've had to admit that the algorithm works pretty decently as a terrain generator. The Diamond-Square algorithm can be used to force a mountain with a peak, by "seeding" the array with values. More than just the center point of the array must be seeded to achieve acceptable results. He complains of some inherent creasing problems as well. But you judge for yourself. The algorithm is originally described by Fournier, Fussell, and Carpenter 4.)
    
    算法思想是這樣的:你以一個大而空的代表點的二維數組陣列開始。有多大?簡單起見,這個陣列應該是個正方形。而且每一維的尺寸應該是2的幾次方再加1(比如33x33, 65x65, 129x129等等)。將四個角落的點設成相同的值。看看你得到的東西,它是一個正方形。

    舉一個簡單的例子,讓我們使用一個 5x5的數組。(我們將在後面談到這幅圖像,所以別把它忘到腦後。)在圖a中,被賦值的四個角落的點用黑色高亮顯示:


    下面是算法的起點,共分爲兩個階段:


     “菱形”階段:利用構成正方形的四個點,在這個正方形的中點,也就是兩條對角線交匯的地方,生成一個隨機值。中點值等於,四個邊角點值求平均後再加上這個隨機值。當在網格中有多個正方形時,這樣就會爲你產生菱形。(這裏講的很模糊,我也是摸索了很久才弄明白作者指的是什麼意思。這裏給一點提示,看着圖b,用眼睛把左邊的兩個邊角點,連同那個中心點連起來,順序是:左上點->中心點->左下點。這樣你就得到了一個菱形的右半部分。看出來了嗎?它的左半部分呢?其實是循環順延到了右邊,也就是右上點->中心點->右下點構成了這個菱形的左半部分。不過,在作者看來,半個菱形就可以算是菱形了。所以,這裏一共有四個菱形:左上點->中心點->左下點、右上點->中心點->右下點、左上點->中心點->右上點、左下點->中心點->右下點。)


     “正方形”階段:利用構成菱形的四個點,在這個菱形的中點生成一個和上一步相同取值範圍區間的隨機值。同樣這個中點值等於,四個邊角點值求平均後再加上這個隨機值。這樣就又會給你產生正方形。(看着圖c,左上點->中上點->中心點->左中點構成了一個正方形,容易看出該圖一共排布着4個這樣的正方形。)

    所以,如果你“種下”1個正方形並且只做了一輪這樣的循環後,你將最終得到4個正方形。兩輪循環你就得到16個正方形,三輪就有64個了。它增長的很快。正方形的數量等於 2^(i*2) ,i是循環迭代的輪數。(The number of squares generated is equal to 2^(I+2), where I is the number of iterations through the recursive subdivision routine.)(必須指出,我翻的和原文並不一致,尤其是在那個關鍵公式上有些出入。原因是,無論我怎麼理解,我都搞不清楚2^(I+2)的意義。如果2^(I+2)指的是得到的正方形的個數, I無論指什麼意思,都不能滿足1,4,16,64這樣的增長規律。因爲I增長必是按1累加的,即1,2,3這樣的順序,可是當I等於1時,結果就會出現8,這是不可能的。如果出現預期的結果,那麼I必須以2累加,即0,2,4等等,可是這時I又表明什麼意思呢?無法解釋。唯有把公式解釋爲2^(I*2),一切疑問都煙消雲散。這時I指的是迭代次數。I=0時(還沒有迭代),只有1個正方形;I=1時(第一次迭代),出現4個正方形;I=2時(第二次迭代),16個正方形;I=3時(第三次迭代),64個正方形。這樣就很好,一切都非常和諧。關於此疑問我已給原作者發了詢問郵件。不過,對於他是否能夠回覆,我和兄弟們一樣期待。。。。-_-!)

    現在我們對照前面5幅圖,逐一看看當我們執行“菱形-正方形”算法的那兩個階段時發生的一切。

    對於第一輪的“菱形”階段,我們在這個數組的中心(即正方形的中心點)生成了一個值,這個值是基於四個邊角值得出的。我們計算四個邊角值的平均值(如果這四個值相等的話,這一步其實是不必做的),然後加上一個範圍在區間[-1.0,1.0]裏的隨機值。如圖b所示,新產生的點用黑色表示,已經存在的點則用灰色表示。

    接下來是“四邊形”階段。我們在和上一階段相同的隨機數取值範圍內生成一個隨機值。在這個階段一共有四個菱形。它們全部相交在原正方形的中心點。所以我們要分別計算這四個菱形的中心點。對每一個菱形,求它的四個邊角值的平均值,再加上那個隨機值,就是各自中心點的值。如圖c所示,新產生的點用黑色表示,已經存在的點則用灰色表示。

    這就是第一回合。如果你用線把這九個點連起來,你會得到像下面這樣的線框圖: 

 


    現在讓我們進入第二回合。我們再次以“菱形”階段開始。第二回合在兩個方面區別於第一回合。首先,我們這次有了4個正方形,而不是1個。所以我們需要分別計算這4個正方形的中心點。其次,同時這也纔是最關鍵的,這次隨機值的取值範圍區間要被縮減。比如拿這個例子來說,我們設置的H值爲1的話,那麼隨機值的取值範圍就會從(-1.0 ,1.0),縮減到(-0.5,0.5)。如圖d所示,我們計算的四個正方形的中心點用黑色表示。

    最後,我們進行這一回合的“正方形”階段。因爲一共有12個菱形中心點,所以我們現在需要計算12個新值。圖e中用黑色標出了它們。

    現在,這個數組中的25個點已經全部生成。現在我們也許會得到如下的一幅線框圖:

 

 

    如果分配一個更大的數組,我們就能繼續循環更多次的回合,在每輪迴閤中增加更多的細節。比如,經過5輪迴合,我們的表面也許看起來會像這樣:

 


    我之前提過,數組中每一維的尺寸應該是2的幾次方再加1。這是因爲,一個二維數組需要的浮點型數的個數是(2^I+1)^2,8輪迴合下來,就會需要一個257x257大的數組。這對於32位的IEEE浮點數類型來說,會吃掉比256KB還多的內存。

    這是一筆很大的開銷。使用字符類型代替浮點類型會有所幫助。示例程序用的是浮點型。但如果內存對你來說真的很吃緊的話,你就得用字符型,用一個範圍在-128到128的有符號字符類型來替換示例程序中的浮點型應該很簡單。但是當你生成它們時,要小心夾緊這些值:即便在第一回合你把它們限制在-128到128內,在隨後的循環中,仍然有可能超出這個範圍,由此產生一個“溢出條件”。這在當H較小時尤其可能發生。

    示例程序告訴我們另一種解決空間問題的方法。首先我們分配一個很大的數組,然後用“菱形-正方形”算法來爲其賦值。之後用“從頂部到底部”的正投影視角將這個數組渲染成一幅圖像,然後把這幅圖像作爲紋理貼圖來回讀給第二個只需佔用較小內存的數組。一旦渲染出來的那副圖像從幀緩衝區那裏被回讀完畢,你就可以釋放第一個佔用很大內存的數組了,儘管示例程序並沒有這麼做(指釋放內存)

    下面是這種紋理貼圖的一個例子:

 

 

      這張圖人爲地把峯頂塗成白色,把峽谷塗成綠色,二者之間的部分則塗成灰色。你可以用示例程序提供的的源代碼來隨意實施你自己的配色方案。

(下面這段位於兩個分界線的文字我仍然附上原文(汗顏....),並且原文在上,翻譯在下,上下對比以作參考。原因是作者文字之簡單但意圖之模糊相當打擊本人信心。我覺得自己沒有能力很好地理解他到底是什麼意思,但依然試圖翻一翻,並且根據意思和一些有限的推理作出大膽假設。這段信息後面又有一段位於兩個分界線的文字,那是我對此段文字的一些理解。如果只看原文就懂的朋友可繞過理解文字不看,不過我希望還是看看,幫忙校正一下是不是我在扯淡。)


-------------------------------------------

Earlier I had mentioned that there are advantages to implementing this routine iterative rather than recursive. Here's why: A recursive implementation might take the form:

 Do diamond step.
 Do square step.
 Reduce random number range.
 Call myself four times.

That's a nice simple implementation, and I have no doubt that it would work. But it requires that some points be generated with insufficient data. Why? After the first pass, you'll be called upon to perform the square step without having all four corners of a diamond. 

Instead, I've implemented this iteratively, and the basic pseudocode looks like this:

 While the length of the side of the squares 
 is greater than zero {
 Pass through the array and perform the diamond 
 step for each square present.
 Pass through the array and perform the square 
 step for each diamond present.
 Reduce the random number range.
 }

This eliminates the problem of missing diamond corners found in the recursive implementation. But you'll run into this problem again anytime you generate a point on the edge of the array. It turns out this is only a concern in the square step. You can easily overcome this and simultaneously make the surface wrappable by taking into account that one of the four diamond corner points lies on the other side of the array. (Another key to making the surface wrappable is to remember to seed the four corners with the same value.)


之前我說過,用“迭代”比用“遞歸”更好。因爲,一個“遞歸”手法的實現很可能是這樣的形式:

 

執行一步“菱形”階段的操作
執行一步“正方形”階段的操作
縮減隨機值取值範圍區間

調用自己四次

 


這個實現很簡單,而且我也不懷疑它能運行。但這個方法的漏洞是,其中有一些點要在數據不足的情況下被生成。爲什麼呢?在執行完第一階段後,你還尚未得到菱形的全部四個邊角值時,就要執行“正方形”階段了。

相反,我用“迭代”手法來實現這個算法的話,基本的僞代碼類似這樣:

 

當正方形的變長大於0時{
傳遞數組,爲當前每個正方形執行“菱形”階段。
傳遞數組,爲當前每個菱形執行“正方形”階段。
縮減隨機值取值範圍區間

}

 

這樣就解決了用“遞歸手法實現時“丟失的菱形角落”的漏洞問題。但是(即便你用“迭代”方法實現)只要你在數組的邊緣上生成一個點時你會再次碰上這個問題。不過這個問題只會在“正方形”階段出現。你可以很容易地解決這個問題,你可以把這個表面“包裹”起來,假想菱形其中的一個角落(丟失的那個角落)落在了數組的另一邊上。(另外使表面可以被“包裹”的一個關鍵是(在數組的四個角上)“種下”相同的四個值。)

-------------------------------------------


(以下是我對上面文字做的理解)

-------------------------------------------

我對作者的話存在兩點疑惑:

1. 作者先說遞歸也是行得通的( I have no doubt t hat it would work. ),但又說這樣會生成一些“殘疾”的點,因爲這些點是在數據不足時產生的( it requires that some points be generated with insufficient data.)。點是生成了,但卻是殘疾的,那麼這個實現方法到底算“成”還是算“不成”呢?作者告訴我們,採用“迭代”比採用“遞歸”更優,開始我以爲是性能效率上的比較,因爲我們知道,“非遞歸”的開銷確實要小於“遞歸”的開銷,“遞歸”僅僅是代碼上看上去簡潔了不少而已。但是作者介紹到這裏時,卻到了傷筋動骨的地步,簡直感覺如果用“遞歸”的話就會殘疾了似的。到底是怎麼回事呢,弄不清楚。

2. 對比遞歸的僞代碼和迭代的僞代碼,我沒看出什麼本質的不同,如果是區別在於後者傳遞了數組的話,我完全可以把數組設計成一個全局變量,然後同樣傳遞給遞歸函數,這不同樣可以辦到麼?因此我想這不是遞歸的死穴。另外,我不清楚爲什麼要“調用自己四次”??


經過一段時間思考,我忽然得出這樣的猜測(不保證正確,這裏正是需要大家拍磚校正的地方):

     這就像是當初學數據結構時關於“圖的遍歷”的兩種方法似的:深度遍歷和廣度遍歷。這裏的“遞歸”實現就像是深度遍歷一樣,它會盯住一個根“菱形-正方形”結構,逐步分解成更小的“菱形-正方形”結構(通過不停壓棧),直到到達最小(這裏調用四次即到達最小),然後返回上一級,然後遍歷完這一級所有的結構,然後再返回上一級,直到棧被彈空。這樣帶來的問題是,當要求其中某一個結構的中心點時,會遇到圍繞這個結構的四個邊角,有一個角落點是不屬於這個大結構的情況(那個角落點屬於鄰近的那個大結構裏的點),但是由於“遞歸”的算法,你在當前大結構未被返回前,是無法計算鄰近的那個大結構的點的值的,因此就帶來了“丟失的角落”問題。這屬深度遍歷的特性所致,可謂死穴。

     而迭代就像廣度遍歷那樣,先把當前一級的所有結構都遍歷完畢,才深入到下一層去遍歷更小的結構,這樣當下一層的小結構需要用到鄰近的邊角值來計算中心點時,因這些邊角值屬上一層的點,已被完全計算出來,因此就不存在什麼障礙了,這樣就很好地解決了遞歸的死穴問題。

-------------------------------------------



 

    下面是一個“菱形的一個角落出現在數組的另一邊”的例子。圖中,我們在“正方形”階段生成了一個點,而它恰好落在了數組的邊上。灰色表示的是包含了菱形四個角落的位置。這四個位置的值需要求均,以此作爲中間新值的基值,圖中那個新值用黑色表示。

 


    注意,圖中用黑色表示的有兩個點。實際上它們是相等的。每次你在“正方形”階段計算一個落在數組邊上的點時,記得在數組對面的一邊也要存儲它一遍。爲了能夠無縫對接,這些點必須完全相同。

    這就意味着,在前面的圖e中,我們其實不必把那12個點值全都各自算一遍,因爲其中有4個點值是另外一邊對應點值的重複。因此我們只有8個點需要計算。

    我將給有興趣的讀者留一個練習:修改示例程序中的源代碼,讓它在不需要計算“數組邊上的重複點值”的情況下也能運行。(“計算重複點值”這件事)對於算法來說真的不是必須的,只是我恰好在代碼裏寫成了那個樣子。

    如果直到現在你還沒有運行過示例程序,也許現在你該打開它看一看。這個程序的初始狀態是一個經過兩次迭代而生成的表面。它是用“線框模式”渲染的,也就是簡單地用線段連接起數組中的每個點而已。數組中的值被當作Y值對待,而每個點的X軸和Z軸的座標信息在數組解析的過程中動態生成。用三角形(作爲基本圖元)可以很容易渲染圖像,你可以把每一個正方形刨切成兩個三角形(連接正方形的對角線即可得到)。“三角形”一般來說是用來渲染圖形的非常優秀的基本圖元,因爲它永遠都是“凸面”的,而且保證三個點絕對在同一平面。

    點擊進入“View Options ”對話框。改變“Random seed ”值可以讓你生成一個不同的表面,稍稍調高點“Iterations”值,可以爲表面增加更多的細節。代碼中規定這個值的上限爲10,此上限對於我的32M內存,奔騰Pro系統而言已經有點吃不消了,不管怎麼看都是黑黑的一片(which is a little much for my 32 Meg RAM Pentium Pro system and just looks black anyway. )(Meg ,應該是megabyte的縮寫,意爲“兆字節”)。 (五年後,人們會在新的處理器和更高分辨率的屏幕上運行這段代碼,然後他們會奇怪幹嘛我非要把這個值卡在10以內....) (實際上,這篇文章是寫於997年。如今12個年頭過去了。我在我的1GM內存,Pentium M 1.73GHz,ATI X600 的機子上運行這段程序,把“Iterations”調到10後得到的依然是一片黑色.....)

    第一個H值控制着表面的粗糙度。默認值設爲0.7 。 試着調高或調低這個值,然後注意觀察結果。

    是的,這個算法偶爾會出現局部“突起”和一些“褶皺”。但是我比較喜歡這些超現實的“突起”效果,而“褶皺”並不明顯,這取決於你從哪個角度去觀察它,也和你從上面飛過它時有多快有關係(估計是用來作爲飛行模擬的場景時)( and the creasing is not obvious depending on what angle you are viewing it from, or how fast you're flying over it.)


天空雲圖


    現在我們知道如何來生成一個表面。我們既可以生成並渲染成千上萬個三角形,也可以把一個高解析度的紋理貼圖貼到一個低解析度的表面上。(這兩種方式)無論採用哪一種,都可以營造出非常酷的效果。但是現在我們該怎麼生成頭頂上的雲團呢?實際上它比你想的要簡單的多。

    用“菱形-正方形”算法進行“鑲嵌”過的數組,非常適合用來描繪一幅天空雲團的紋理貼圖。只不過這時數組的值,不再代表高度圖裏Y軸的值,而是表示雲團的透明度。最小的值表示“最藍”,是天空中最晴朗的部分;而最大值表示“最白”,是天空中雲層最密佈的部分。

    輕輕鬆鬆解析完數組,你將得到這樣一幅紋理貼圖:

    

 

    這幅圖和本文前面說過的“高度圖”很類似,但我把低端和高端的值保持在某個範圍,以此來生成一片“雲彩斑斕”的天空(but I have clamped the low and high values to create patches of clear and clouded sky.)

    運行示例程序,你也可以生成像上面的這樣一幅圖像。選擇 Select rendering type 下拉框,選中2D mesh / clouds選項。(默認狀態下它看上去類似像素那樣一格一格的,試着把 Cloud iterations值調到8或者更高看看)。接下來設定不同的H值,這樣就能得到不同的雲團效果。

    如果你回到這篇文章剛開頭的地方,你會發現我把所有曾討論過的東西都組裝在了第一張圖裏。天空是用上面展示的紋理貼圖生成的,在一個八棱錐上平鋪了多次(The sky is made with a texture map as shown above, tiled multiple times over an eight-sided pyramid. )(意思是,天空的效果是用八棱錐做的一個天空盒,然後在上面平鋪了很多這個貼圖而做成的)。表面的幾何體是用高解析度紋理貼圖渲染的。該紋理貼圖由“自頂向下”的正投影視角渲染。這幅圖像經過“回讀”後被當作一張新貼圖使用。(就是前面說過的那個節省內存的技巧。)

    示例程序會展示幾乎所有在本文中所提到的圖像。


其他方法


    你也許想對“地形生成”施加更多的控制,超過示例程序所提供的功能。比如說,你也許想在開始的幾輪迴閤中先在數組中“種下”一些你自己規定的值,這樣像山、峽谷、等等什麼的都可以按你的設計來放置。然後再用“菱形-正方形”算法來填充剩餘的細節。

    不要像Miller那樣,想要生成一座山,卻僅僅只對數組的中心點賦一個很大的值。想要產生合理的結果,你起碼應該要(用“菱形-正方形”算法)爲這個數組“播種”兩到三輪。

    你很容易就可以通過改寫代碼,讓數組中已經賦了值的元素不再被分配新值。首先,初始化你的數組,比如說把每個元素都設爲-10。接着,最初的幾輪迭代用你自己指定的值來“播種”。然後,用那段改寫過的代碼來只對值爲-10的元素分配新值。最初的幾輪迭代不會生成任何值,因爲你自己規定好的值已經在那了。隨後的迭代中,纔會根據你規定好的這些值來生成新值。

    如何創造自己的“種子”? 如果你想要的形狀是某種已知的可以用數學公式描述的形式,比如正弦曲線,那麼直接使用公式就可以生成這些值。但要是別的情況,就得想一些“創造性的辦法”來完成它了。我曾見過的一個辦法是用“灰度值”來填充你自己的高度圖。把“灰度值”和“高度值”進行一一映射,然後存儲在你的數組中。接下來再用“菱形-正方形”算法來增加更多的細節。

    除了使用“菱形-正方形”算法,你還可以使用許多其他算的法達到同樣的目的。

(以下兩段話我不太明白什麼意思,不保證翻譯質量,遂附上原文。並請大牛拍磚指正。)

   (有一種算法是這樣的:)在二維數組中隨機選取一段範圍,爲該範圍內每一個元素賦一個很小的值。不停地重複這個過程,分別爲每一段隨機選中的範圍裏的所有元素都增加一個很小的值。這樣也會產生不錯的結果,但是該算法的複雜度並不是線性的。如果計算的時間成本對你來說無所謂,那我鼓勵你試試這種算法。

(With successive random addition, a random region of the 2D array is incremented by a small amount. Repeat this process over and over, adding a small amount into each randomly chosen region of the array. This generates good results but is not computationally linear. If compute time is not a concern, I encourage you to try this algorithm out.)


    另一種相似的算法是在數組中製造出一個“斷層”,然後爲其中一邊的所有元素賦值,就像“地震”發生時那樣。這個過程也需要重複好多次。同樣該算法的複雜度也不是線性的,而且你必須迭代幾輪才能得到像樣的結果。

(Another similar method involves making a "fracture" across the array and incrementing one side of it, as if an earthquake had occurred. Again, repeat several times. This is also not a linear algorithm and takes several passes to get acceptable results.)


欲瞭解更多其他的方法,請查閱後面的參考書目。


第二部分:關於示例程序及其源代碼


安裝


    示例程序及其源代碼打包在一個zip文件裏。使用你最喜歡的zip解壓縮軟件來解開它。如果你還沒有一個zip解壓縮軟件,可以試試PKware(我們使用winrar就能打開。)

    源代碼使用OpenGL API 作爲渲染的圖形接口。如果你的機器還沒有安裝OpenGL,你應該先得到它。Microsoft和SGI 都提供了支持Windows 95 的OpengGL版本。但我勸你選用SGI的,因爲它在性能和程序健壯性方面都比Microsoft的強出去不少。示例代碼鏈接的是SGI實作版本。因爲SGI和Microsoft選擇了不同的名字來命名它們的DLL文件,所以代碼使用的是SGI的DLL文件。

    下面是“傻瓜式製作酷圖像”的說明嚮導。

雙擊 Fractal Example 圖標。
打開View Options 對話框。
從Select render type下拉菜單中選擇2D mesh / rendered。
在 Iterations 框中輸入4 。
在 Tile 框中輸入3 。
點擊 OK 。
 

使用示例程序


    默認狀態下,程序顯示的是二維網格線框模式。它已經是用“菱形-正方形”算法生成了的。兩輪迭代能產生16個正方形。(Two passes were made over the surface resulting in eight squares.)(這又是個錯誤,原文中說是8個正方形,但是按照正確的思路,兩輪下來能產生16個正方形。運行程序你也能看到,確實是16個而不是8個。這裏應該是作者的一個筆誤。)

    你可以使用箭頭鍵來改變視角。使用←和→鍵可以旋轉場景。按住Shift鍵再按↑和↓鍵可以令場景向上或向下運動。鬆開Shift鍵再按↑和↓鍵可以令場景向前或向後運動。

    現在解釋View Options 對話框。你可以在菜單欄裏點擊View 選項,或者按Ctrl-O(注意是O不是0)來打開它。這裏你可以改變你要想看到的東西,以及爲“如何生成它們”設定其屬性值。對話框看起來像這個樣子:


    Select rendering type 下拉框菜單控制你要顯示什麼圖形。1D midpoint displacement 渲染了一條用“中點替換”算法渲染的線段。

    所有標明有2D mesh 的類型都是利用“菱形-正方形”算法生成的一幅圖像。2D mesh / lines 是用線框模式渲染的一個表面。2D mesh / rendered 展示了一個“二維地形”,這個面是用當前紋理貼圖(就是選項“2D mesh teximage ”中生成的那個貼圖)貼出來的,而天空是用“雲團貼圖”貼出來的。2D mesh / clouds 允許你只觀察“雲團貼圖”。這是一個簡單的二維高度圖,藍色表示小值,白色表示大值。2D mesh teximage  允許你只觀察在rendered 模式中展出的那個覆蓋地形的紋理貼圖。這是一個“自頂向下”觀察的正投影表面。使用了不同的顏色來區別這個面不同的“高度值”。

    你可以使用對話框右半邊的參數,以此控制這些圖形的生成。

    參數Tile 表示需要“排布”多少個表面或者線段。默認值是1。對於線段,這個參數決定了會平鋪多少條單位爲“1”的線段;而對於表面,把這個參數設爲2則表示有2x2個單位爲“1”的表面,3則表示有3x3個。如此等等。

    參數Random seed設置了一個新的“隨機種子”,以此可以創造出不同的表面。

    參數 Iterations 決定會迭代多少次。數值越大則細節越多。默認值是2,這個參數的取值範圍是從1到10。

    你可以調節參數Cloud iterations來設置“雲團貼圖”的細節度。類似地,你也可以調節參數eximage iterations 來設置“地形貼圖”的細節度。

    注意,對話框裏一共有三個H值。第一個H值用來生成表面,第二個H值用來“雲團貼圖”,第三個H值用來生成“地形貼圖”。

    當渲染類型選擇1D midpoint displacement 或者2D mesh / lines時,勾選Antialiased lines 複選框會採用抗鋸齒模式。

    勾選Invert colors複選框會反轉背景和線條顏色。

    當渲染類型選擇2D mesh / rendered時,Texture linear複選框控制“雙線性紋理濾波方式”的開關。勾選此開關後,結果的生成會耗時更長,但最後的圖像素質也會更高。

代碼結構


    文件fractmod.c 和文件fractmod.h是這個示例程序的C程序代碼。 他們包含了“分形生成”模塊。

    微軟1996年11月的期刊上(http://www.microsoft.com/msj)刊載了一篇文章,提到 CFractalExampleView 類是COpenGLView 類的派生類。COpenGLView類的作者是Ron Fosner,他把這個類描述成原成熟類的“黑客版本”(The COpenGLView class was written by Ron Fosner, who describes it as a hacked version of his fully-blown COpenGLView class)。(直到現在,微軟期刊上仍有這篇文章,點擊這裏查看。)想要知道到底怎麼回事,你得買一本他寫的書《Programming for Windows 95 and Windows NT》,該書由Addison-Wesley出版。

    該COpenGLView類有一個虛擬成員函數RenderScene,在CFractalExampleView類中我們重寫了它。這裏我們做了大部分的渲染工作。這個函數首先檢查“渲染類型”。當設置爲2D mesh / lines 或者1D midpoint displacement時,那麼仍由原RenderScene來處理。否則的話我們調用另一個函數。

    函數CFractalExampleView::OnViewDialog 用來生成View Options 對話框,以及在對話框類和CFractalExampleView類之間設定和檢索數據。

    函數CFractalExampleView::OnInitialUpdate負責把所有CFractalExampleView類中的成員變量設定爲默認值(包括對話框中的值)。

    其實真沒什麼必要去更深地解釋代碼是如何工作的。我假設你是一名很有能力的程序員,而我也盡了最大努力來全面地討論這些代碼了。如果你不熟悉OpenGL,那你也許很想知道的是,那些以“gl”開頭的函數其實都是OpenggL API 的調用。微軟的Visual C++ 對此API有一些有限的文件。

    我請你爲這個程序增添一個功能(There is one feature that is just begging to be added to this code)。在文件Fractal ExampleView.cpp中,有一個預置的常量DEF_HEIGHT_SCALE (there is a preprocessor constant called DEF_HEIGHT_VALUE)(作者筆誤,其實應該是DEF_HEIGHT_SCALE),這個值會被傳給位於fractmod.c文件的控制“分形生成”的函數裏,以此來達到“縮放”高度值的目的。其實它應該被設定爲一個由對話框來控制的變量。請放心地添加這個功能。(意思是,作者認爲這個值如果體現在對話框裏,直接由用戶控制的話會更好更合適,但他的程序沒有這麼做。因此他希望讀者來完成這個功能)。


參考書目


1 Miller, Gavin S. P., The Definition and Rendering of Terrain Maps. SIGGRAPH 1986 Conference Proceedings (Computer Graphics, Volume 20, Number 4, August 1986).

2 Voss, Richard D., FRACTALS in NATURE: characterization, measurement, and simulation. SIGGRAPH 1987 course notes #15.

3 Peitgen, Jurgens, and Saupe, Chaos and Fractals, New Frontiers of Science. Springer-Verlag, 1992.

4 Fournier, A., Fussell, D., Carpenter, L., Computer Rendering of Stochastic Models, Communications of the ACM, June 1982.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章