在先前的畫光系列中,實現實體幾何、反射、折射等效果,可是最大的一個缺陷是複雜度過高。當採樣是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
咱們先來看頂層調用(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的分類討論。。),討論光線起點與兩個圖形的位置關係。
第一,討論光線起點不在兩圓中。
分六種狀況(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相交,這時狀況複雜了
只考慮兩種不相交的狀況,如圖。反映在代碼中就是兩個break。
渲染的結果有所不一樣,發光圖形自己的顏色是白色的,這是由於定義時的顏色是RGB(2.0f,1.0f,1.0f),最終採樣經平均後呈現時仍是RGB(2.0f,1.0f,1.0f),作了一個截斷以後就是RGB(1.0f,1.0f,1.0f)即白色。
本文重點即兩圓之間的交集、差集的解析法實現,還有直線與圓的相交算法。
程序下載:bajdcc/GameFramework