用C++畫光(一)——優化


寫在前面

在先前的畫光系列中,實現實體幾何、反射、折射等效果,可是最大的一個缺陷是複雜度過高。當採樣是1024時,渲染時間直線上升(用4線程),以致好幾個小時才能完成一副做品,實現太慢。然而,當我看到用C++畫光(一)這篇文章時,我有了一些思路。html

我想到了【遊戲框架系列】簡單的圖形學(一)系列文章中的思路,對啊,何須用SDF去慢慢逼近呢?用現成的解析幾何算法去作不是更快嗎?git

過了一番摸索,終於有了題圖。github

要注意的地方

檢測圓與直線相交的算法,我從用JavaScript玩轉計算機圖形學(一)光線追蹤入門 - Milo Yip - 博客園抄來:算法

intersect : function(ray) {
        var v = ray.origin.subtract(this.center);
        var a0 = v.sqrLength() - this.sqrRadius;
        var DdotV = ray.direction.dot(v);

        if (DdotV <= 0) {
            var discr = DdotV * DdotV - a0;
            if (discr >= 0) {
                var result = new IntersectResult();
                result.geometry = this;
                result.distance = -DdotV - Math.sqrt(discr);
                result.position = ray.getPoint(result.distance);
                result.normal = result.position.subtract(this.center).normalize();
                return result;
            }
        }

        return IntersectResult.noHit;
    }

但這裏有所不一樣:3D中的光線追蹤是有視角的,也就是說,光線來自同一個點,即攝像機的位置,所以,光線的起點不會在幾何圖形內部!!而2D中,咱們的渲染方式有所不一樣,光線來自2D區域中的每個點上,所以,光線起點可能在圖形內部框架

因此修改後的代碼是這樣:ide

Geo2DResult Geo2DCircle::sample(vector2 ori, vector2 dir) const
{
    // 圓上點x知足: || 點x - 圓心center || = 圓半徑radius
    // 光線方程 r(t) = o + t.d (t>=0)
    // 代入得 || o + t.d - c || = r
    // 令 v = o - c,則 || v + t.d || = r

    // 化簡求 t = - d.v - sqrt( (d.v)^2 + (v^2 - r^2) )  (求最近點)

    // 令 v = origin - center
    auto v = ori - center;

    // a0 = (v^2 - r^2)
    auto a0 = SquareMagnitude(v) - rsq;

    // DdotV = d.v
    auto DdotV = DotProduct(dir, v);

    //if (DdotV <= 0)
    {
        // 點乘測試相交,爲負則同方向

        auto discr = (DdotV * DdotV) - a0; // 平方根中的算式

        if (discr >= 0)
        {
            // 非負則方程有解,相交成立
            // r(t) = o + t.d
            auto distance = -DdotV - sqrtf(discr); // 得出t,即攝影機發出的光線到其與圓的交點距離
            auto distance2 = -DdotV + sqrtf(discr);
            auto position = ori + dir * distance; // 代入直線方程,得出交點位置
            auto normal = Normalize(position - center); // 法向量 = 光線終點(球面交點) - 球心座標
            if (a0 <= 0 || distance >= 0)// 這裏不同!!
                return Geo2DResult(this, a0 <= 0, distance, distance2, position, normal);
        }
    }

    return Geo2DResult(); // 失敗,不相交
}

相交算法實質上是用參數方程代入求解,得出參數t,其實就是距離。有一種狀況是無效的,須要注意,就是當距離爲負且光線起點不在圖形內部時,這樣一種解是無效的,由於咱們的線是射線測試

爲何渲染速度變快了

原來的方法是SDF,即不斷迭代,最終逼近相交點。這種方法的問題就是迭代的次數太多,每次迭代都進行了一樣的計算。優化

優化以後,計算相交點,咱們直接用解析法,一個方程就能搞定,更棒的是,咱們能夠求出相交的最近點和最遠點(這很重要!)。this

方法的不一樣影響了渲染時間的多少。spa

怎樣安排代碼

bajdcc/GameFramework

咱們先來看頂層調用(https://github.com/bajdcc/GameFramework/blob/master/CCGameFramework/base/pe2d/Render2DScene2.cpp#L72):

root = Geo2DFactory:: or (
	Geo2DFactory:: and (
		Geo2DFactory::new_circle(1.3f, 0.5f, 0.4f, color(2.0f, 1.0f, 1.0f)),
		Geo2DFactory::new_circle(1.7f, 0.5f, 0.4f, color(2.0f, 1.0f, 1.0f))),
	Geo2DFactory:: sub (
		Geo2DFactory::new_circle(0.5f, 0.5f, 0.4f, color(1.0f, 1.0f, 2.0f)),
		Geo2DFactory::new_circle(0.9f, 0.5f, 0.4f, color(1.0f, 1.0f, 2.0f))));

結果就是題圖,代碼定義了兩個圖形,一個是兩圓相交and,一個是兩圓sub,兩個圖形用or串聯起來。
固然,後面還能夠用重載讓代碼更簡潔。

直線與圓相交算法在https://github.com/bajdcc/GameFramework/blob/master/CCGameFramework/base/pe2d/Geometries2D.cpp#L163中,上面已貼過。

如何實現兩個圖形的交、並、差?

這裏纔是本文重點,而實現這功能用了不少時間。

並:

if (op == t_union)
{
    const auto r1 = obj1->sample(ori, dst);
    const auto r2 = obj2->sample(ori, dst);
    return r1.distance < r2.distance ? r1 : r2;
}

很好解釋,直線掃到兩個圖形上,若是沒交點,那麼distance就是無窮大,若是有交點,就取距離較近的圖形。


交:

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.distance < r2.distance)
                {
                    if (r2.distance2 > r1.distance && r2.distance > r1.distance2)
                        break;
                    return r2;
                }
                if (r2.distance < r1.distance)
                {
                    if (r1.distance2 > r2.distance && r1.distance > r2.distance2)
                        break;
                    return r1;
                }
                break;
            case 1: // B
                if (r1.distance < r2.distance2)
                    return r1;
                break;
            case 2: // A
                if (r2.distance < r1.distance2)
                    return r2;
                break;
            case 3: // A and B
                return r1.distance > r2.distance ? r1 : r2;
            default:
                break;
            }
        }
    }
}

代碼中distance是最近交點(較小根),distance2是最遠交點(較大根)。

交就複雜得多,首先,光線必須與兩個圖形都有交點,其次,分四種狀況(我喜歡這樣寫兩個bool的分類討論。。),討論光線起點與兩個圖形的位置關係。

第一,討論光線起點不在兩圓中。

v2-a8e39207779b638b88597e5aa825fa46_r交集的狀況

分六種狀況(C{2,4}=6),其中兩種狀況爲不相交,剩下四種狀況爲相交,代碼中就是這個思路。

第二,討論光線起點在B中,顯而易見,交點就是A的邊界

第三,討論光線起點在A中,顯而易見,交點就是B的邊界

第四,討論光線起點在A交B中,這裏必相交


差:

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
        return r1;
    case 3: // A and B
        if (r2.inside)
        {
            if (r1.distance2 > r2.distance2)
            {
                auto r(r2);
                r.body = r1.body;
                r.inside = false;
                r.distance = r.distance2;
                return r;
            }
            break;
        }
        if (r1.inside)
        {
            return r1;
        }
        if (r2.distance < r1.distance)
        {
            if (r1.distance2 < r2.distance2)
            {
                break;
            }
            auto r(r2);
            r.body = r1.body;
            r.inside = false;
            r.distance = r.distance2;
            return r;
        }
        return r1;
    default:
        break;
    }
}

差的實現也不簡單,討論射線與兩圓的相交狀況:

第一:射線與兩圓都不相交,那射線與A-B也不相交

第二:射線與B相交,與A不相交,那射線與A-B也不相交

第三:射線與A相交,與B不相交,射線一定與A-B相交

第四:射線與A交B相交,這時狀況複雜了

v2-ee1d94d97a1d47334211b10db554f916_r差集的狀況

只考慮兩種不相交的狀況,如圖。反映在代碼中就是兩個break。

小結

渲染的結果有所不一樣,發光圖形自己的顏色是白色的,這是由於定義時的顏色是RGB(2.0f,1.0f,1.0f),最終採樣經平均後呈現時仍是RGB(2.0f,1.0f,1.0f),作了一個截斷以後就是RGB(1.0f,1.0f,1.0f)即白色。

本文重點即兩圓之間的交集、差集的解析法實現,還有直線與圓的相交算法。

程序下載:bajdcc/GameFramework

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

相關文章
相關標籤/搜索