Unity動態構建mesh繪制多邊形算法流程分析和實踐


前言

先說一下,寫這篇博文的動機,原文的博主代碼寫的十分瀟灑,以至於代碼說明和注釋都沒有,最近恰逢看到,所以以此博文來分析其中的算法和流程
參考博文:https://blog.csdn.net/linxinfa/article/details/78816362

github網址:https://github.com/linxinfa/Unity-ArbitraryPolygonMesh

先復習一下線代
向量的混合積的數學意義是:兩個向量叉乘的結果是一個新向量,這個新向量垂直於原向量組成的平面,並且新向量的長度等於原向量合成的平行四邊形的面積

向量的混合積是三個向量組成的平行六面體的體積。叉乘可以看成高是單位長度的平行六面體的體積,也就是其平行四邊形的面積

操作步驟

  • 在場景中創建多個幾何體作為mesh多邊形的頂點
  • 在父物體上,創建幾個子物體,順時針擺放這些物體

image.png
image.png

image.png

代碼步驟

  • 先通過叉乘計算多邊形的面積,叉乘嚴格按照Inspector面板的擺放順序進行計算,保證是同一順序時針,通過判斷計算面積結果正負性(Unity采用右手坐標系,但在此處還是加上判斷),提前設置頂點(V[n])的順序
    // 計算多邊形的面積,按照同一順序進行叉乘
    private float Area()
    {
        // n = 5
        int n = m_points.Count;
        float A = 0.0f;
        Vector2 pval = Vector2.zero;
        Vector2 qval = Vector2.zero;
        for (int p = 0; p < n; p++)
        {
            pval = m_points[p];
            qval = m_points[(p + 1) % n];
            A += pval.x * qval.y - qval.x * pval.y;
        }
        return (A * 0.5f);
    }
  • 按照面積的正負性,改變新創建的頂點數組順序
        if (Area() > 0)
        {
            // 0 1 2 3 4
            for (int v = 0; v < n; ++v)
                V[v] = v;
        }
        else
        {
            // 將頂點順序逆過來
            // 4 3 2 1 0
            for (int v = 0; v < n; ++v)
                V[v] = (n - 1) - v;
        }
  • 此處改了一點參考博文的代碼,因為n邊形的一個頂點出發只能引出(n-2)條對角線,每次枚舉三個連續頂點,判斷是否能組成三角形(Snip),因為我們提前設置了每個頂點所能發射的最多對角線數量,所以此處無需考慮三個連續頂點重復采用,如果count為0了,可以理解有nv次機會判斷是否形成三角形,如果沒更新count(也就是沒走if里面)則直接返回
        int nv = n;
        int count = nv;
        int u, w;
        // n邊形的一個頂點出發只能引出(n-2)條對角線
        for (int v = nv - 1; nv >= 3;) 
        {
            // 形成不了三角形時會走這個return
            if ((count--) <= 0)
                return indices.ToArray();
                
            // 連續三個頂點 不超過nv
            u = v;
            v = u + 1;
            w = u + 2;

            u %= nv;
            v %= nv;
            w %= nv;
            
            // u v w連續三個頂點能組成三角形,且保證順序是合理的
            if (Snip(u, v, w, nv, V))
            {
                int a, b, c;
                a = V[u];
                b = V[v];
                c = V[w];

                // 將頂點按照順序放入list中
                indices.Add(a);
                indices.Add(b);
                indices.Add(c);

                // 做了代碼修改,原文寫法比較麻煩,將V數組中的值從后往前挪一位
                for (int s = v; s + 1 < nv; ++s)
                    V[s] = V[s + 1];

                nv--;
                // 原博文是 count = 2 * nv 但其實沒必要
                count = nv;
            }
        }
  • 判斷是否能形成三角形
    private bool Snip(int u, int v, int w, int n, int[] V)
    {
        Vector2 A = m_points[V[u]];
        Vector2 B = m_points[V[v]];
        Vector2 C = m_points[V[w]];

        // 面積如果小於Mathf.Epsilon(接近0的最小正浮點數),就為不能切分成三角形
        // AB向量叉乘AC向量結果為ABC三頂點形成封閉圖形的面積的兩倍,判斷是否能形成三角形
        if (Mathf.Epsilon > ((B.x - A.x) * (C.y - A.y)) - ((B.y - A.y) * (C.x - A.x))) 
            return false;
        for (int p = 0; p < n; ++p)
        {
            // 直到p為沒選到的邊
            if ((p == u) || (p == v) || (p == w))
                continue;

            Vector2 P = m_points[V[p]];
            // 防止線邊交叉
            if (InsideTriangle(A, B, C, P))
                return false;
        }
        return true;
    }
  • 判斷線邊交叉情況,一般情況下返回值都是false(cCROSSap一般與aCROSSbp和bCROSScp正負性相反,因為選點的時候故意為之的),除非線邊交叉,發生叉乘的正負性發生了改變
  • 如下圖這種情況,如果注釋了InsideTriangle()函數,網格會生成不出來,因為出現了線邊交叉情況,導致可能繪制頂點的順序有順時針有逆時針,從而導致只有繪制面到了背面或者直接就不出來效果
  • 👇叉乘結果

\[ BC×BP > 0\\ CA×CP > 0\\ AB×AP > 0 \]

image.png

  • 👇正常情況

\[ BC×BP > 0\\ CA×CP > 0\\ AB×AP < 0 \]

image.png

    private bool InsideTriangle(Vector2 A, Vector2 B, Vector2 C, Vector2 P)
    {
        float ax, ay, bx, by, cx, cy, apx, apy, bpx, bpy, cpx, cpy;
        float cCROSSap, bCROSScp, aCROSSbp;

        ax = C.x - B.x; ay = C.y - B.y;
        bx = A.x - C.x; by = A.y - C.y;
        cx = B.x - A.x; cy = B.y - A.y;
        apx = P.x - A.x; apy = P.y - A.y;
        bpx = P.x - B.x; bpy = P.y - B.y;
        cpx = P.x - C.x; cpy = P.y - C.y;

        // BC向量與BP向量叉乘
        aCROSSbp = ax * bpy - ay * bpx;
        // CA向量與CP向量叉乘
        bCROSScp = bx * cpy - by * cpx;
        // AB向量與AP向量叉乘
        cCROSSap = cx * apy - cy * apx;

        // 保證叉乘的順序都是一致的
        // 此處算的結果一般都為false
        // 畫個圖就可以得知 
        if (((aCROSSbp >= 0.0f) && (bCROSScp >= 0.0f) && (cCROSSap >= 0.0f)))
        {
            Debug.Log($"A.x = {A.x}, A.y = {A.y}");
            Debug.Log($"B.x = {B.x}, B.y = {B.y}");
            Debug.Log($"C.x = {C.x}, C.y = {C.y}");
            Debug.Log($"P.x = {P.x}, P.y = {P.y}");
        }
        // Debug.Log(((aCROSSbp >= 0.0f) && (bCROSScp >= 0.0f) && (cCROSSap >= 0.0f)));
        return ((aCROSSbp >= 0.0f) && (bCROSScp >= 0.0f) && (cCROSSap >= 0.0f));
    }

效果

Alt Text
Alt Text
Alt Text


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM