高效生成均勻分佈的點:快速泊松碟採樣算法實現(Fast Poisson Disc Sampling)

快速泊松碟採樣算法實現(Fast Poisson Disc Sampling)

前(fei)言(hua)

最近在看一些隨機地圖生成算法,涉及到生成Voronoi圖,這需要提前在一個平面內隨機生成一堆的點,這些點還要滿足隨機而且儘量均勻分佈在平面上。一般文章都提到採用Lloyd Relaxation算法,不過這個算法比較複雜,消耗也比較大,後來看到這個快速泊松碟採樣算法,也是用於生成一堆均勻分佈的點的,而且算法複雜度在O(N)O(N)
算法的說明論文可以參考這裏。個人覺得描述得很不(fan)清(ren)晰(lei),後來我看了別人的視頻說明才真正瞭解這個算法,其實算法本身很簡單,也很直觀,下面會講解下。完整代碼我放在個人Github上。

算法說明

下面以2維平面爲例:
假設我們需要在一個寬高爲(width,height)(width,height)的平面內平均生成一堆的點,且這些點之間的距離不能小於rr
我們可以先從平面內隨機選一個點,然後在這個點附近隨機找一些點,並判斷這些點是否合法,合法的話則在這些點附近繼續隨機尋找,直到找不到合法點爲止。

算法簡單說起來就是這樣,下面詳細說下細節。

  1. 爲了保證能儘量填滿整個平面, 隨機找點時,採用與中心點距離爲[r,2r)[r,2r)的圓環內找,這個距離能保證找的點距離自身大於rr,且不會離得太遠,能填滿整個平面。
  2. 怎麼判斷一個點的附近已經找不到合法點了?算法定義了一個常量kk,對於每個點,我們嘗試在它附近隨機找kk次,如果都找不到,那麼就認爲這個點附近已經沒有合法點。
  3. 怎麼快速判定隨機找出的點是否合法?這個是算法的關鍵,可以採用一些空間劃分方法來做(遊戲場景也會經常用到),首先將平面劃分成mmnn列的格子,每個格子都保存了格子內部的點。這樣當我需要判斷一個點是否合法時,我只要和附近的格子內的點做判斷即可。
  4. 那怎麼確定每個格子的大小?我們儘量讓每個格子內部最多隻能有1個點,這樣數據結構就會簡單很多。怎麼做到呢?我們假設每個格子都是正方形,那正方形內部距離最遠的點就是對角線的2個點,所以我們只要保證正方形的對角線長度大於等於rr,則正方形內部任意2個點之間的距離肯定小於rr,從而保證每個正方形內部肯定最多隻能有1個點。假設正方形邊長爲aa,對角線長度爲rr,那麼有:a2+a2=r2a^2+a^2=r^2,那麼a=r2a=\frac{r}{\sqrt{2}}

代碼

public static class Algorithm
{
    public static List<Vector2> Sample2D(float width, float height, float r, int k = 30)
    {
        return Sample2D((int)DateTime.Now.Ticks, width, height, r, k);
    }

    public static List<Vector2> Sample2D(int seed, float width, float height, float r, int k = 30)
    {
        // STEP 0

        // 維度,平面就是2維
        var n = 2;

        // 計算出合理的cell大小
        // cell是一個正方形,爲了保證每個cell內部不可能出現多個點,那麼cell內的任意點最遠距離不能大於r
        // 因爲cell內最長的距離是對角線,假設對角線長度是r,那邊長就是下面的cell_size
        var cell_size = r / Math.Sqrt(n);

        // 計算出有多少行列的cell
        var cols = (int)Math.Ceiling(width / cell_size);
        var rows = (int)Math.Ceiling(height / cell_size);

        // cells記錄了所有合法的點
        var cells = new List<Vector2>();

        // grids記錄了每個cell內的點在cells裏的索引,-1表示沒有點
        var grids = new int[rows, cols];
        for (var i = 0; i < rows; ++i) {
            for (var j = 0; j < cols; ++j) {
                grids[i, j] = -1;
            }
        }

        // STEP 1
        var random = new Random(seed);

        // 隨機選一個起始點
        var x0 = new Vector2(random.Range(width), random.Range(height));
        var col = (int)Math.Floor(x0.x / cell_size);
        var row = (int)Math.Floor(x0.y / cell_size);

        var x0_idx = cells.Count;
        cells.Add(x0);
        grids[row, col] = x0_idx;

        var active_list = new List<int>();
        active_list.Add(x0_idx);

        // STEP 2
        while (active_list.Count > 0) {
            // 隨機選一個待處理的點xi
            var xi_idx = active_list[random.Range(active_list.Count)]; // 區間是[0,1),不用擔心溢出。
            var xi = cells[xi_idx];
            var found = false;

            // 以xi爲中點,隨機找與xi距離在[r,2r)的點xk,並判斷該點的合法性
            // 重複k次,如果都找不到,則把xi從active_list中去掉,認爲xi附近已經沒有合法點了
            for (var i = 0; i < k; ++i) {
                var dir = random.insideUnitCircle();
                var xk = xi + (dir.normalized * r + dir * r); // [r,2r)
                if (xk.x < 0 || xk.x >= width || xk.y < 0 || xk.y >= height) {
                    continue;
                }

                col = (int)Math.Floor(xk.x / cell_size);
                row = (int)Math.Floor(xk.y / cell_size);

                if (grids[row, col] != -1) {
                    continue;
                }

                // 要判斷xk的合法性,就是要判斷有附近沒有點與xk的距離小於r
                // 由於cell的邊長小於r,所以只測試xk所在的cell的九宮格是不夠的(考慮xk正好處於cell的邊緣的情況)
                // 正確做法是以xk爲中心,做一個邊長爲2r的正方形,測試這個正方形覆蓋到的所有cell
                var ok = true;
                var min_r = (int)Math.Floor((xk.y - r) / cell_size);
                var max_r = (int)Math.Floor((xk.y + r) / cell_size);
                var min_c = (int)Math.Floor((xk.x - r) / cell_size);
                var max_c = (int)Math.Floor((xk.x + r) / cell_size);
                for (var or = min_r; or <= max_r; ++or) {
                    if (or < 0 || or >= rows) {
                        continue;
                    }

                    for (var oc = min_c; oc <= max_c; ++oc) {
                        if (oc < 0 || oc >= cols) {
                            continue;
                        }

                        var xj_idx = grids[or, oc];
                        if (xj_idx != -1) {
                            var xj = cells[xj_idx];
                            var dist = (xj - xk).magnitude;
                            if (dist < r) {
                                ok = false;
                                goto end_of_distance_check;
                            }
                        }
                    }
                }

                end_of_distance_check:
                if (ok) {
                    var xk_idx = cells.Count;
                    cells.Add(xk);

                    grids[row, col] = xk_idx;
                    active_list.Add(xk_idx);

                    found = true;
                    break;
                }
            }

            if (!found) {
                active_list.Remove(xi_idx);
            }
        }

        return cells;
    }
}
// 測試代碼
class Program
{
    static void Main(string[] args)
    {
        var width = 1024;
        var height = 1024;
        var r = 50f;
        var points = Algorithm.Sample2D(width, height, r);

        var image = new Bitmap(width, height);
        using (var graphics = Graphics.FromImage(image)) {
            graphics.FillRectangle(Brushes.Black, 0f, 0f, width, height);

            var dot_r = 3f;
            var pen = new Pen(Color.DarkRed, 2f);
            foreach (var p in points) {
                graphics.FillEllipse(Brushes.Yellow, p.x - dot_r, p.y - dot_r, 2f * dot_r, 2f * dot_r);
                graphics.DrawEllipse(pen, p.x - r / 2f, p.y - r / 2f, r, r);
            }
        }

        image.Save("out.png");
    }
}

輸出的圖片:可以看到黃點分佈隨機且比較平均,且任意2個黃點之間的距離都小於rr(紅色圓的半徑是r/2r/2
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章