用C++畫光(三)——色散

v2-adf07208b4a0a141e07a9f84364cebae_r

寫在前面

源碼:https://github.com/bajdcc/GameFramework/blob/master/CCGameFramework/base/pe2d/Render2DScene5.cppgit

本文主要內容:github

  1. 三角形的渲染
  2. 聚光效果的實現
  3. 色散的簡易版實現

三角形的渲染

在上一文中,主要介紹了矩形的渲染,其實三角形也跟它差很少,無非就是判斷線與線的關係罷了。緩存

三角形的數據結構:數據結構

// 三角形
class Geo2DTriangle : public Geo2DShape
{
public:
    Geo2DTriangle(vector2 p1, vector2 p2, vector2 p3, color L, color R, float eta, color S);
    ~Geo2DTriangle() = default;

    Geo2DResult sample(vector2 ori, vector2 dir) const override;

    vector2 get_center() const override;

    vector2 center, p1, p2, p3;
    vector2 n[3];
};

要注意的地方:ide

  1. 三角形的中心點(重心)要計算出來
  2. 緩存三角形三條邊的法線
  3. 確保三個點p1~p3的按順時針方向排列的

頂點排序與法線設置測試

// 假定三頂點是順時針方向
const auto p12 = p2 - p1;
const auto p13 = p3 - p1;
if (p12.x * p13.y - p12.y * p13.x < 0) // 確保點一、二、3是順時針
{
    const auto tmp = p2;
    p2 = p3;
    p3 = tmp;
}
n[0] = p2 - p1;
n[0] = Normalize(vector2(n[0].y, -n[0].x));
n[1] = p3 - p2;
n[1] = Normalize(vector2(n[1].y, -n[1].x));
n[2] = p1 - p3;
n[2] = Normalize(vector2(n[2].y, -n[2].x));

怎樣知道三個點是順時針排列的呢?本質上是求一個點在另外兩個點造成的線段的哪一側。this

求點P在直線L的左側仍是右側?能夠用叉乘法,咱們只要知道叉乘結果的符號就能夠了。spa

三角形的採樣方法相似於矩形的:線程

Geo2DResult Geo2DTriangle::sample(vector2 ori, vector2 dir) const
{
    const vector2 pts[3] = { p1,p2,p3 };

    static int m[3][2] = { { 0,1 },{ 1,2 },{ 2,0 } };
    float t[2];
    vector2 p[2];
    int ids[2];
    int cnt = 0;
    for (int i = 0; i < 3 && 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],有交點,在內
            return Geo2DResult(this, true,
                Geo2DPoint(t[0], p[0], n[ids[0]]),
                Geo2DPoint(t[1], p[1], n[ids[1]]));
        case 2: // t[0],有交點,在內
            return Geo2DResult(this, true,
                Geo2DPoint(t[1], p[1], n[ids[1]]),
                Geo2DPoint(t[0], p[0], n[ids[0]]));
        case 3: // 雙正,有交點,在外
            if (t[0] > t[1])
            {
                return Geo2DResult(this, false,
                    Geo2DPoint(t[1], p[1], n[ids[1]]),
                    Geo2DPoint(t[0], p[0], n[ids[0]]));
            }
            else
            {
                return Geo2DResult(this, false,
                    Geo2DPoint(t[0], p[0], n[ids[0]]),
                    Geo2DPoint(t[1], p[1], n[ids[1]]));
            }
        default:
            break;
        }
    }
    return Geo2DResult();
}

聚光效果

要作一個色散就要一束平行的光,實現很簡單,限制角度!code

咱們在圓的採樣方法中,作一個判斷:當光線來的角度不在聚光燈有效範圍內時,就返回黑色。

Geo2DResult Geo2DCircle::sample(vector2 ori, vector2 dir) const
{
    auto v = ori - center;
    auto a0 = SquareMagnitude(v) - rsq;
    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 position2 = ori + dir * distance2;
            auto normal = Normalize(position - center); // 法向量 = 光線終點(球面交點) - 球心座標
            auto normal2 = Normalize(position2 - center);
            if (a0 > 0 && angle && !(A1.x * dir.y < A1.y * dir.x && A2.x * dir.y > A2.y * dir.x))
            { // 判斷三條線之間的時針順序
                return Geo2DResult();
            }
            return Geo2DResult((a0 <= 0 || distance >= 0) ? this : nullptr, a0 <= 0,
                Geo2DPoint(distance, position, normal),
                Geo2DPoint(distance2, position2, normal2));
        }
    }

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

色散效果

色散其實就是不一樣頻率的光在介質內的折射率不一樣,咱們就簡化一下,按照RGB修改折射率,如:紅光=原折射率,綠光=原折射率+0.1,等。

對於沒有明確修改折射率(默認爲1.0)的圖形,不對它作色散檢查。

if (r.body->eta == 1.0f) // 不折射
{
    // 按照先前的折射方法,不變!
}
else // 色散測試
{
    const auto eta = r.inside ? r.body->eta : (1.0f / r.body->eta);
    const auto k = 1.0f - eta * eta * (1.0f - idotn * idotn);
    if (k >= 0.0f) // 能夠折射,不是全反射
    {
        const auto a = eta * idotn + sqrtf(k);
        const auto refraction = eta * d - a * normal;
        const auto cosi = -(DotProduct(d, normal));
        const auto cost = -(DotProduct(refraction, normal));
        refl = refl * (r.inside ? fresnel(cosi, cost, eta, 1.0f) : fresnel(cosi, cost, 1.0f, eta));
        refl.Normalize();
        //下面不同了
        color par;//求三個維度的份量和
        sum.Set(0.0f);//光源的光就不歸入計算
        par.Add(trace5(pos - BIAS * normal, refraction, depth + 1));//加上紅光的份量
        auto n = par.Valid() ? 1 : 0;
        par.g *= ETAS;//ETAS=0.1 對紅光份量而言,綠和藍份量就削減它
        par.b *= ETAS;
        for (int i = 1; i < 3; ++i)//求藍光和綠光份量
        {
            //ETAD=0.1   折射率:綠=紅+0.1 藍=紅+0.2
            const auto eta0 = r.inside ? (r.body->eta + ETAD * i) : (1.0f / (r.body->eta + ETAD * i));
            const auto k0 = 1.0f - eta0 * eta0 * (1.0f - idotn * idotn);
            if (k >= 0.0f) // 能夠折射,不是全反射
            {
                const auto a0 = eta0 * idotn + sqrtf(k0);
                const auto refraction0 = eta0 * d - a0 * normal;
                auto c = trace5(pos - BIAS * normal, refraction0, depth + 1);//作折射計算
                if (c.Valid())
                {
                    if (i == 1)
                    {
                        c.r *= ETAS;//削減其餘兩個顏色份量
                        c.b *= ETAS;
                    }
                    else
                    {
                        c.r *= ETAS;
                        c.g *= ETAS;
                    }
                    n++;//若是這一份量不爲黑色,就有效,加一,本來要加最終值作下平均的,如今暫不用它
                }
                par.Add(c);// 加上藍和綠份量
            }
        }
        sum.Add((refl.Negative(1.0f)) * par);//再加上三個折射份量的和
    }
    else // 不折射則爲全內反射
        refl.Set(1.0f);

局部掃描

當光源很亮(RGB>10f)時,僅256的採樣還不能有很好的效果,用下面的方法:

static color sample5(float x, float y) {
    color sum;
    for (auto i = 0; i < N; i++) {
        const auto a = PI2 * (i + float(rand()) / RAND_MAX) / N;
        const auto c = trace5(vector2(x, y), vector2(cosf(a), sinf(a)));
        if (c.Valid())
        {
            color par;
            for (auto j = 0; j < NP; j++) {//進一步計算
                const auto a0 = PI2 * (i + (j + float(rand()) / RAND_MAX) / NP) / N;
                const auto c0 = trace5(vector2(x, y), vector2(cosf(a0), sinf(a0)));
                par.Add(c0);
            }
            sum.Add(par * (1.0f / NP));
        }
    }
    return sum * (1.0f / N);
}

當第一層抖動採樣結果有效時,作第二層抖動採樣,精度更高。


最終結果1080P,一層採樣數=512,二層採樣數=8,雙核四線程渲染用時差很少半小時。

進一步更真實的話,我只想到再增長一些折射測試,將本來的RGB份量擴展爲七彩色,轉換用RGB跟HSL的,其中的問題就是七彩色各份量並不正交,如何將它們整合起來還待研究。

題圖的設定爲RGB份量的折射率遞增爲0.1,也就是說1.4~1.6,顏色削減爲0.1。另外,光源的光也不是嚴格的平行光,更優的效果還須要不斷調整參數。

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

相關文章
相關標籤/搜索