Drawing light with C++ (3) - dispersion

v2-adf07208b4a0a141e07a9f84364cebae_r

write in front

Source code: https://github.com/bajdcc/GameFramework/blob/master/CCGameFramework/base/pe2d/Render2DScene5.cpp

The main content of this article:

  1. Rendering of triangles
  2. Realization of spotlight effect
  3. Simple implementation of dispersion

Rendering of triangles

In the previous article, I mainly introduced the rendering of rectangles. In fact, triangles are similar to it. It is nothing more than judging the relationship between lines.

Triangle data structure:

// 三角形
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];
};

Points to pay attention to:

  1. The center point (center of gravity) of the triangle is calculated
  2. Cache the normals of the three sides of the triangle
  3. Make sure that the three points p1~p3 are arranged clockwise

Vertex sorting and normal settings

// assume the three vertices are clockwise 
const  auto p12 = p2 - p1;
 const  auto p13 = p3 - p1;
 if (p12.x * p13.y - p12.y * p13.x < 0) // ensure point 1 , 2, 3 are clockwise
{
    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));

How do you know if the three dots are arranged clockwise? Essentially, which side of a line segment formed by two other points is found.

Is point P to the left or right of line L? We can use cross multiplication, we just need to know the sign of the result of the cross multiplication.

Triangles are sampled similarly to rectangles:

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: // double inverse, no intersection, 
            break outside ;
         case 1: // t[1], intersection, 
            return Geo2DResult( this , true ,
                Geo2DPoint(t[0], p[0], n[ids[0]]),
                Geo2DPoint(t[1], p[1], n[ids[1]]));
        case 2: // t[0], with intersection, 
            return Geo2DResult( this , true ,
                Geo2DPoint(t[1], p[1], n[ids[1]]),
                Geo2DPoint(t[0], p[0], n[ids[0]]));
        case 3: // double positive, with intersection, outside 
            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();
}

spotlight effect

To do a dispersion, you need a beam of parallel light, the implementation is very simple, limit the angle!

In the circle sampling method, we make a judgment: when the angle from which the light comes is not within the effective range of the spotlight, it returns black.

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; // equation in square root

        if (discr >= 0)
        {
            // If the equation is non-negative, the equation has a solution, and the intersection is established. 
            // r(t) = o + td 
            auto distance = -DdotV - sqrtf(discr) ; 
            auto distance2 = -DdotV + sqrtf(discr);
             auto position = ori + dir * distance; // Substitute into the line equation to get the intersection position 
            auto position2 = ori + dir * distance2;
             auto normal = Normalize(position - center); / / normal vector = ray end point (spherical intersection) - sphere center coordinates 
            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))
            { // Determine the clock order between the three lines 
                return Geo2DResult();
            }
            return Geo2DResult((a0 <= 0 || distance >= 0) ? this : nullptr, a0 <= 0,
                Geo2DPoint(distance, position, normal),
                Geo2DPoint(distance2, position2, normal2));
        }
    }

    return Geo2DResult(); // fail, disjoint
}

Dispersion effect

Dispersion is actually the difference in the refractive index of light with different frequencies in the medium. Let's simplify it and modify the refractive index according to RGB, such as: red light = original refractive index, green light = original refractive index + 0.1, etc.

For graphs that do not explicitly modify the index of refraction (1.0 by default), do not check for dispersion.

if (r.body->eta == 1.0f) // no refraction
{
    // According to the previous refraction method, unchanged!
}
else  // dispersion test
{
    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) // can be refracted, not totally reflected
    {
        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();
        //The following is different 
        color par; //Find the component sum of the three dimensions 
        sum.Set(0.0f); //The light of the light source is not included in the calculation 
        par.Add(trace5(pos - BIAS * normal, refraction, depth + 1)); //Add the red light component 
        auto n = par.Valid() ? 1 : 0;
        par.g *= ETAS; //ETAS=0.1 for the red component, the green and blue components just cut it
        par.b *= ETAS;
        for ( int i = 1; i < 3; ++i) // find blue and green components
        {
            //ETAD=0.1 Refractive index: green=red+0.1 blue=red+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) // refraction, not total reflection
            {
                const  auto a0 = eta0 * idotn + sqrtf(k0);
                 const  auto refraction0 = eta0 * d - a0 * normal;
                 auto c = trace5(pos - BIAS * normal, refraction0, depth + 1); // do the refraction calculation 
                if ( c.Valid())
                {
                    if (i == 1)
                    {
                        cr *= ETAS; //cut the other two color components
                        c.b *= ETAS;
                    }
                    else
                    {
                        c.r *= ETAS;
                        c.g *= ETAS;
                    }
                    n++; //If this component is not black, it is valid, add one, originally to add the final value to do the average, now do not use it
                }
                par.Add(c); // add blue and green components
            }
        }
        sum.Add((refl.Negative(1.0f)) * par); //Add the sum of the three refraction components
    }
    else  // no refraction is total internal reflection
        refl.Set(1.0f);

Partial scan

When the light source is very bright (RGB>10f), only 256 samples can not have a good effect, use the following method:

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++) { // further calculations 
                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);
}

When the first-layer jitter sampling result is valid, the second-layer jitter sampling is performed, and the accuracy is higher.


The final result is 1080P, the number of samples for the first layer = 512, the number of samples for the second layer = 8, and the dual-core and four-thread rendering takes about half an hour.

If it is more realistic, I just think of adding some refraction tests, expanding the original RGB components into seven colors, and using RGB and HSL for conversion. The problem is that the components of the seven colors are not orthogonal. How to integrate them? To be studied.

The setting of the title image is that the refractive index of the RGB component is incremented by 0.1, that is, 1.4~1.6, and the color is reduced to 0.1. In addition, the light of the light source is not strictly parallel light, and the parameters need to be continuously adjusted for better effects.

Backed up by https://zhuanlan.zhihu.com/p/32486185 .

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325056235&siteId=291194637