程式設計師們都喜歡創造一些東西,但是,還有什麼會比建立一個世界更讓人感到驚喜?想想Minecraft, Terragen, Skyrim,以及以前的每一個都會使用一些生成分形地形的飛行模擬器。今天,我們要來探索如何使用漂亮而又簡單的QPSO演算法 (diamond-square algorithm),到時,你也可以扮演上帝![Demo] [Source]
程式設計師往往是懶惰的(從經驗來說的話),而懶惰的一個很好地“副作用”就是這真的是可以避免一些(重複)工作的很不錯的方式。既然這樣, 與其花上乏味的幾個小時來建立可能是很蹩腳的岩石表面,不如我們在思想上教會電腦岩石到底是什麼。為了達到我們的目的,我們會生成分形或者形狀,而這些形狀會以越來越小的變化不斷重複。
我並不能以某種方式來證明地形確實是分形的,但是這種方法看起來真的很不錯,因此你可以信任這種方法。
立體地圖
我們會將我們的地形儲存為一個簡單地立體地圖:一個由地形在任意給定的x,y座標上的高度值所組成的二維陣列。這是一個比較簡單的資料結構,用我們喜歡的canvas,webgl,interpretive dance等技術都可以來渲染這些高度值。最大的限制是我們不能在地形中表示有垂直的洞的形狀,比如洞穴,隧道或者橋樑。
1 2 3 4 5 |
function Terrain(detail) { this.size = Math.pow(2, detail) + 1; this.max = this.size - 1; this.map = new Float32Array(this.size * this.size); } |
對任何尺寸的網格你都可以應用上面的演算法來生成地圖,但是對於一個由2的整數冪加1的網格組成方形來說它是最簡單的。我們將使用x、y和z軸相同大小的值,在一個多維資料集中實現我們的地形。我們把相關的細節(detail)(即網格的數量)轉化成了2的整數冪加1,因此更多的網格數量需要有更大的資料集。
對應的演算法
想法是這樣的:取一個平面的方形。把它分成4個子方形,然後把這4個子方形的中心向上或向下隨機的偏移一定量。把這些子方形再分成更多的子方形並且重複上面的步驟,每一次都將偏移的量減少,這樣第一次的偏移會有最大的效果而後面的偏移都會提供更小的細節(起伏程度)。
這就是中點置換演算法(Midpoint displacement algorithm)。我們的菱形演算法基於類似的原則,但是生成了看起來會更自然的結果。與其只是把方格分成更多的子方格,不如在分成子方格與分成子的菱形方格之間做個替換。
1.設定各個角的座標
首先,我們要設定各個角的座標值來作為“種子”值,它會影響後面的呈現。我們會將所有的角落從資料集的一半的位置開始:
1 2 3 4 |
this.set(0, 0, self.max / 2); this.set(this.max, 0, self.max / 2); this.set(this.max, this.max, self.max / 2); this.set(0, this.max, self.max / 2); |
2.將地圖分塊
現在,我們將會遞迴的來看立體地圖的越來越小的分塊。在每一個分塊的過程中,我們會把地圖分成方塊,並在方形階段更新它們的中心點。然後,我們會把地圖分成菱形,並在菱形階段再次更新它們的中心點。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
divide(this.max); function divide(size) { var x, y, half = size / 2; var scale = roughness * size; if (half < 1) return; for (y = half; y < self.max; y += size) { for (x = half; x < self.max; x += size) { square(x, y, half, Math.random() * scale * 2 - scale); } } for (y = 0; y <= self.max; y += half) { for (x = (y + half) % size; x <= self.max; x += size) { diamond(x, y, half, Math.random() * scale * 2 - scale); } } divide(size / 2); } |
saele變數保證了隨著我們分塊的次數的增多,偏移量是不斷減小的。對於每一次分塊,我們將當前的size變數與roughness相乘,roughness決定了我們的地形是平滑的(該變數值趨近於0時)還是起伏的(該變數值趨近於1時)。
3.形狀
兩種形狀(方形和菱形)的工作機制是類似的,但是要從不同的點來繪製資料。在方形階段,要在應用隨機偏移之前獲取四個角的座標的平均值,在菱形階段要在執行隨機偏移之前獲取四個邊緣點的座標的平均值。
1 2 3 4 5 6 7 8 9 |
function diamond(x, y, size, offset) { var ave = average([ self.get(x, y - size), // top self.get(x + size, y), // right self.get(x, y + size), // bottom self.get(x - size, y) // left ]); self.set(x, y, ave + offset); } |
渲染
演算法只是給了我們資料,我們可以用很多種方式來渲染資料。我們將整合一連串的渲染技巧來渲染一個位於canvas元素上的柵格化的,等距的,3d形式的地形圖上。
從back到front
首先,我們將建立巢狀的迴圈從我們地圖的“back”(y = 0) 到“front”(y=this.size)來繪製矩形。如果你要渲染一個簡單地,平的,自頂向下的方形,那麼要執行的迴圈是一樣的。
1 2 3 4 5 6 7 8 9 10 11 12 |
for (var y = 0; y < this.size; y++) { for (var x = 0; x < this.size; x++) { var val = this.get(x, y); var top = project(x, y, val); var bottom = project(x + 1, y, 0); var water = project(x, y, waterVal); var style = brightness(x, y, this.get(x + 1, y) - val); rect(top, bottom, style); rect(water, bottom, 'rgba(50, 150, 200, 0.15)'); } } |
光亮和陰影
我們對於集合對映的原始方法提供了一個很好的視覺文理。通過比較當前的高度值和下一個點的高度值,我們會找到一個坡度。坡度高的一側我們用較亮的矩形來填充,另一側則用較暗的矩形來填充。
1 2 |
var b = ~~(slope * 50) + 128; return ['rgba(', b, ',', b, ',', b, ',1)'].join(''); |
等軸投影
我們可以從正面來繪製每一樣東西,但是,在將方塊轉為3d之前,先將它轉為菱形看起來會更有趣。等軸投影將左上角和右下角在檢視的中間對齊。
1 2 3 4 5 6 |
function iso(x, y) { return { x: 0.5 * (self.size + x - y), y: 0.5 * (x + y) }; } |
透視投影
我們將使用一個同樣簡單的3d投影轉換我們的x,y,z值為在二維視角螢幕上的平面影象。
所有的透視投影的基本想法都是用水平和垂直的位置除以深度,那樣的話更高的深度的渲染就會更接近於原點(例如,越遠的物體就會看起來越小)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function project(flatX, flatY, flatZ) { var point = iso(flatX, flatY); var x0 = width * 0.5; var y0 = height * 0.2; var z = self.size * 0.5 - flatZ + point.y * 0.75; var x = (point.x - self.size * 0.5) * 6; var y = (self.size - point.y) * 0.005 + 1; return { x: x0 + x / y, y: y0 + z / y }; } }; |
把所有的內容整合起來
首先,我們用我們所期望的細小平面建立了一個地形例項。然後我們生成了它的立體地圖,提供了一個位於0和1之間的roughness值。最後,我們把地形繪製到了canvas上。
1 2 3 |
var terrain = new Terrain(9); terrain.generate(0.7); terrain.draw(canvasContext, width, height); |
試試看
請點選下面連結檢視最終效果:來自另一個世界的地形(otherworldly terrain)
接下來
如果你跟我一樣,這個簡單的演算法會讓你渴望去創造一個線上的自制夢幻風景,一個基於飛行器的第一人稱射擊遊戲,模擬釣魚或者一個大型多人線上角色扮演遊戲等等。這個單一的立方體式的,基於canvas的demo非常需要擴充套件。
下面的幾項我希望你能去嘗試:
1.用WebGl渲染;
2.跟隨高度的變化,高度越小的地形越平滑(像沙子),高度越大的越崎嶇;
3.投射暗影,而不是簡單地基於斜坡來產生陰影;
4.新增一個功能,生成洞穴和隧道。
按照慣例,你還可以在這裡看到這個想法。
相關的工作
現在有很多人在研究這個演算法,並且他們建立了很多很多很酷的東西。而且,黑客新聞討論也展示了很多相關的真的非常不可思議的例子。這裡有幾個比較突出的:
- WebGL rendering implementation by callum
- Objective C implementation by Chris Cieslak
- Processing implementation by Jerome Herr
- Heightmap-based raycaster by namuol
- Procedural demo entry explanation by Inigo Quilez
- Fractional Brownian Motion by rbaravaelle
- Polygonal game map generation by Red Blob Games
討論
可以在 Hacker News 參與討論。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式