用C++畫光(二)——矩形

v2-3d70b56d3d5813599b11200f0d877007_r

在上篇文章的基礎上,作了許多調整,修復了許多BUG。在解決bug的過程當中,我逐漸領悟到一個要領:枯燥地一步步調試太痛苦了,找不到問題的根源!因此我選擇將中間結果打到圖片上。如:git

(注意,裏面的點是我隨便點的,有互動了吧)github

v2-9f98c82c10b472a4a8cfc307d08fa353_r調試光線和最近交點法線算法

v2-ab0ba531f68c10d3374ec47ffbf5cfc5_r調試光線和最遠交點法線數據結構

這就很是爽了!框架

本文分兩個部分,一個是交併差的實現,一個是矩形的實現。ide

基本數據結構

// 點信息
struct Geo2DPoint
{
    Geo2DPoint();
    Geo2DPoint(float distance, const vector2& position, const vector2& normal);

    const Geo2DPoint& operator = (const Geo2DPoint& r);

    float distance{ FLT_MAX }; // 光線起點到交點距離
    vector2 position; // 交點位置
    vector2 normal; // 交點法向量(指向物體外部)
};

// 相交測試
struct Geo2DResult
{
    Geo2DResult();
    Geo2DResult(const Geo2DShape* body, bool inside, Geo2DPoint min_pt, Geo2DPoint max_pt);

    Geo2DResult(const Geo2DResult& r);
    const Geo2DResult& operator = (const Geo2DResult& r);

    const Geo2DShape* body{ nullptr };
    bool inside{ false };
    Geo2DPoint min_pt, max_pt;//交點較小解和較大解的信息
};

每次發出一道光線,須要計算:測試

  1. 若是光線與某物體相交,返回該物體指針body
  2. 光線起點是否在物體內部inside
  3. 光線與物體最近的交點信息,包括交點座標、光線起點到交點的距離、交點法線,若是此時光線起點位於物體內部,那麼交點可能不是最近的(由於這裏的最近指的是解二次方程時的較小根
  4. 光線與物體最遠的交點信息

說明:優化

  • 即便光線與物體未有交點,仍是要計算出全部交點信息
  • 不僅是最近的交點信息很重要,最遠的交點一樣重要

緣由:this

  • 上面的數據結構是爲了解決圖形間交併差的問題!!
  • 如兩圓相交,ABAB的光線路徑,那麼光線與物體相交的兩個點不一樣時是光線離A、B的最近點,因此最遠點的信息是必需要計算的
  • 爲何要用inside?一樣是與A、B相交,光線起點在物體內部和在物體外部所產生的效果是不同的!由於咱們還要計算法線呢!
  • ……所以促成了如今的數據結構

圖形間的交、並、差

上一篇文章中,雖然實現了交併差,可是還不完善:交點信息和法向量沒有計算正確,所以作了調整(並集沒有調整):spa

【計算交集】

https://github.com/bajdcc/GameFramework/blob/master/CCGameFramework/base/pe2d/Geometries2D.cpp#L70

if (op == t_intersect)
{
    const auto r1 = obj1->sample(ori, dst);
    if (r1.body)
    {
        const auto r2 = obj2->sample(ori, dst);
        if (r2.body)
        {
            const auto rd = ((r1.inside ? 1 : 0) << 1) | (r2.inside ? 1 : 0);
            switch (rd)
            {
            case 0: // not(A or B)
                if (r1.min_pt.distance < r2.min_pt.distance)
                {
                    if (r2.min_pt.distance > r1.max_pt.distance) // AABB
                        break;
                    if (r2.max_pt.distance < r1.max_pt.distance) // ABBA
                        return r2;
                    auto r(r2); // ABAB
                    r.max_pt = r1.max_pt;
                    return r;

                }
                if (r2.min_pt.distance < r1.min_pt.distance)
                {
                    if (r1.min_pt.distance > r2.max_pt.distance) // BBAA
                        break;
                    if (r1.max_pt.distance < r2.max_pt.distance) // BAAB
                        return r1;
                    auto r(r1); // BABA
                    r.max_pt = r2.max_pt;
                    return r;
                }
                break;
            case 1: // B
                if (r1.min_pt.distance < r2.max_pt.distance)
                {
                    if (r1.max_pt.distance > r2.max_pt.distance) // ABA
                    {
                        auto r(r1);
                        r.max_pt = r2.max_pt;
                        return r;
                    }
                    else // AAB
                    {
                        auto r(r1);
                        r.max_pt = r1.max_pt;
                        return r;
                    }
                }
                break;
            case 2: // A
                if (r2.min_pt.distance < r1.max_pt.distance)
                {
                    if (r2.max_pt.distance > r1.max_pt.distance) // BAB
                    {
                        auto r(r2);
                        r.max_pt = r1.max_pt;
                        return r;
                    }
                    else // BBA
                    {
                        auto r(r2);
                        r.max_pt = r2.max_pt;
                        return r;
                    }
                }
                break;
            case 3: // A and B
                if (r1.min_pt.distance > r2.min_pt.distance)
                {
                    if (r1.max_pt.distance > r2.max_pt.distance) // BA
                    {
                        auto r(r2);
                        r.min_pt = r1.min_pt;
                        return r;
                    }
                    else // AB
                    {
                        return r1;
                    }
                }
                else
                {
                    if (r2.max_pt.distance > r1.max_pt.distance) // AB
                    {
                        auto r(r1);
                        r.min_pt = r2.min_pt;
                        return r;
                    }
                    else // AB
                    {
                        return r2;
                    }
                }
            default:
                break;
            }
        }
    }
}

【計算差集】

這裏注意的是某些狀況下須要作法向量翻轉

https://github.com/bajdcc/GameFramework/blob/master/CCGameFramework/base/pe2d/Geometries2D.cpp#L171

if (op == t_subtract)
{
    const auto r1 = obj1->sample(ori, dst);
    const auto r2 = obj2->sample(ori, dst);
    const auto rd = ((r1.body ? 1 : 0) << 1) | (r2.body ? 1 : 0);
    switch (rd)
    {
    case 0: // not(A or B)
        break;
    case 1: // B
        break;
    case 2: // A
        if (r1.inside) // AA
        {
            if (r2.max_pt.distance == FLT_MAX)
                return r1;
            if (r1.min_pt.distance > r2.max_pt.distance)
                return r1;
            auto r(r1);
            r.min_pt = r2.max_pt;
            r.min_pt.normal = -r.min_pt.normal;
            return r;
        }
        else
            return r1;
    case 3: // A and B
        if (r1.inside && r2.inside)
        {
            if (r2.max_pt.distance < r1.max_pt.distance) // BA
            {
                auto r(r1);
                r.min_pt = r2.max_pt;
                r.min_pt.normal = -r.min_pt.normal;
                r.inside = false;
                return r;
            }
            else // AB
            {
                break;
            }
        }
        else if (r2.inside)
        {
            if (r1.max_pt.distance > r2.max_pt.distance)
            {
                if (r2.max_pt.distance > r1.min_pt.distance) // ABA
                {
                    auto r(r1);
                    r.min_pt = r2.max_pt;
                    r.min_pt.normal = -r.min_pt.normal;
                    r.inside = false;
                    return r;
                }
                else // BAA
                {
                    return r1;
                }
            }
            else // AAB
                break;
        }
        else if (r1.inside) // BAB
        {
            auto r(r1);
            r.max_pt = r2.min_pt;
            return r;
        }
        else
        {
            if (r1.min_pt.distance < r2.min_pt.distance)
            {
                if (r2.min_pt.distance > r1.max_pt.distance) // AABB
                    return r1;
                if (r2.max_pt.distance < r1.max_pt.distance) // ABBA
                    return r1;
                auto r(r1); // ABAB
                r.max_pt = r2.min_pt;
                r.max_pt.normal = -r.max_pt.normal;
                return r;
            }
            else
            {
                if (r1.min_pt.distance > r2.max_pt.distance) // BBAA
                    return r1;
                if (r1.max_pt.distance < r2.max_pt.distance) // BAAB
                    break;
                auto r(r1); // BABA
                r.min_pt = r2.max_pt;
                r.min_pt.normal = -r.min_pt.normal;
                return r;
            }
        }
    default:
        break;
    }
}

想知道代碼爲何這麼寫,須要拿張紙比劃一下(逃

爲何這麼多if???由於我能夠調試啊(最開始兩張圖),當什麼問題都解決完的時候,代碼就變這麼長了。


矩形的實現

一開始圓的實現很是簡單,由於算個距離很快,法向量就更簡單,而矩形不一樣。

【矩形】

static int SignBit(const float& a)//返回a的符號
{
    if (fabs(a) < EPSILON)
    {
        return 0;//接近0
    }
    return a > 0 ? 1 : -1;
}

static bool IntersectWithLineAB(const vector2& ori, const vector2& dir, const vector2& p1, const vector2& p2, float& t, vector2& p)
{
//利用直線的參數方程計算一直線與另外一直線的交點
    const auto tAB1 = dir.y * (p2.x - p1.x) - dir.x * (p2.y - p1.y);//計算平行
    if (fabs(tAB1) > EPSILON)//不平行必有交點
    {
        t = ((ori.x - p1.x) * (p2.y - p1.y) - (ori.y - p1.y) * (p2.x - p1.x)) / tAB1;//計算距離
        p = ori + dir * t;//計算交點
        return (SignBit(p1.x - p.x) == SignBit(p.x - p2.x)) && (SignBit(p1.y - p.y) == SignBit(p.y - p2.y));//交點是否在p1p2間
    }
    return false;//兩直線平行,無交點
}

Geo2DResult Geo2DBox::sample(vector2 ori, vector2 dir) const
{
    const auto _A = vector2(costheta * -s.x + sintheta * -s.y, costheta * -s.y - sintheta * -s.x);
    const auto _B = vector2(costheta * s.x + sintheta * -s.y, costheta * -s.y - sintheta * s.x);
    const auto A = center + _A;
    const auto B = center + _B;
    const auto C = center - _A;
    const auto D = center - _B;
    const vector2 pts[4] = { A,B,C,D };

    static int m[4][2] = { {0,1},{1,2},{2,3},{3,0} };
    float t[2];//保存兩交點的距離
    vector2 p[2];//保存兩交點的位置
    int ids[2];//保存與矩形哪一條邊相交
    int cnt = 0;
    for (int i = 0; i < 4 && cnt < 2; i++)
    {
        if (IntersectWithLineAB(ori, dir, pts[m[i][0]], pts[m[i][1]], t[cnt], p[cnt]))
        {
            ids[cnt++] = i;
        }
    }
    if (cnt == 2)//有兩個交點
    {
        const auto td = ((t[0] >= 0 ? 1 : 0) << 1) | (t[1] >= 0 ? 1 : 0);
        switch (td)
        {
        case 0: // 雙反,無交點,在外
            break;
        case 1: // t[1],有交點,在內
//只與t1相交,那麼t0確定是另外一交點,p0確定爲負
            return Geo2DResult(this, false,
                Geo2DPoint(t[0], p[0], Normalize(pts[m[ids[0]][0]] + pts[m[ids[0]][1]] - center - center)),
                Geo2DPoint(t[1], p[1], Normalize(pts[m[ids[1]][0]] + pts[m[ids[1]][1]] - center - center)));
        case 2: // t[0],有交點,在內
//只與t0相交,那麼t1確定是另外一交點,p1確定爲負
            return Geo2DResult(this, false,
                Geo2DPoint(t[1], p[1], Normalize(pts[m[ids[1]][0]] + pts[m[ids[1]][1]] - center - center)),
                Geo2DPoint(t[0], p[0], Normalize(pts[m[ids[0]][0]] + pts[m[ids[0]][1]] - center - center)));
        case 3: // 雙正,有交點,在外
            if (t[0] > t[1])//都相交?就看看哪一個交點更近了
            {
                return Geo2DResult(this, false,
                    Geo2DPoint(t[1], p[1], Normalize(pts[m[ids[1]][0]] + pts[m[ids[1]][1]] - center - center)),
                    Geo2DPoint(t[0], p[0], Normalize(pts[m[ids[0]][0]] + pts[m[ids[0]][1]] - center - center)));
            }
            else
            {
                return Geo2DResult(this, false,
                    Geo2DPoint(t[0], p[0], Normalize(pts[m[ids[0]][0]] + pts[m[ids[0]][1]] - center - center)),
                    Geo2DPoint(t[1], p[1], Normalize(pts[m[ids[1]][0]] + pts[m[ids[1]][1]] - center - center)));
            }
        default:
            break;
        }
    }
    return Geo2DResult();
}

其實過程不復雜(我一夜居然能搞定哈哈,這歸功於調試方法的先進性),計算法向量很簡單,就是兩個對角線的疊加方向。。

有幾個注意點:

  • 求符號SignBit本身寫的,用fsignbit致使gg(算我不會用吧),我相信編譯器的優化能力
  • 知矩形中心點、兩軸距離、旋轉角度後,要計算出四點的座標,這時有計算順序,一開始只能將旋轉矩陣應用於(sx,sy)軸向量!不能用於四點的真實座標,至關於先在矩形的本地座標系中應用旋轉,最後才加上矩形自身的位置偏移
  • 爲何要用參數方程作,不用y=kx+b作:若是這時的直線是豎直方向的呢?y=kx+b就不能表達了
  • 求出了交點,要判斷交點是否在線段AB上,作法是x座標和y座標相減判斷同符號,不能用y=kx+b算距離求比值在0~1間,這樣即不正確也不高效

小結

矩形會作以後,多邊形應該都能搞定了,如三角形,只要考慮一下三邊的方向即向(即判斷是在內仍是在外)。

作完以後,在作反射、折射,這簡單了!由於距離呀交點呀法向量所有求出來了。

至於我爲何要在框架中實現這個效果:一者,可視化+互動;兩者,不同凡響;三者,框架要有例子才能證實好用啊。

https://zhuanlan.zhihu.com/p/32251040備份。

相關文章
相關標籤/搜索