4

NOTE: I've edited my code. See below the divider.

I'm implementing refraction in my (fairly basic) ray tracer, written in C++. I've been following (1) and (2).

I get the result below. Why is the center of the sphere black?

Two opaque spheres and a transparent sphere.

The center sphere has a transmission coefficient of 0.9 and a reflective coefficient of 0.1. It's index of refraction is 1.5 and it's placed 1.5 units away from the camera. The other two spheres just use diffuse lighting, with no reflective/refraction component. I placed these two different coloured spheres behind and in front of the transparent sphere to ensure that I don't see a reflection instead of a transmission.

I've made the background colour (the colour achieved when a ray from the camera does not intersect with any object) a colour other than black, so the center of the sphere is not just the background colour.

I have not implemented the Fresnel effect yet.

My trace function looks like this (verbatim copy, with some parts omitted for brevity):

bool isInside(Vec3f rayDirection, Vec3f intersectionNormal) {
    return dot(rayDirection, intersectionNormal) > 0;
}

Vec3f trace(Vec3f origin, Vec3f ray, int depth) {
    // (1) Find object intersection
    std::shared_ptr<SceneObject> intersectionObject = ...;
    // (2) Compute diffuse and ambient color contribution
    Vec3f color = ...;

    bool isTotalInternalReflection = false;
    if (intersectionObject->mTransmission > 0 && depth < MAX_DEPTH) {
        Vec3f transmissionDirection = refractionDir(
            ray,
            normal,
            1.5f,
            isTotalInternalReflection
        );
        if (!isTotalInternalReflection) {
            float bias = 1e-4 * (isInside(ray, normal) ? -1 : 1);
            Vec3f transmissionColor = trace(
                add(intersection, multiply(normal, bias)),
                transmissionDirection,
                depth + 1
            );
            color = add(
                color,
                multiply(transmissionColor, intersectionObject->mTransmission)
            );
        }
    }

    if (intersectionObject->mSpecular > 0 && depth < MAX_DEPTH) {
        Vec3f reflectionDirection = computeReflectionDirection(ray, normal);
        Vec3f reflectionColor = trace(
            add(intersection, multiply(normal, 1e-5)),
            reflectionDirection,
            depth + 1
        );
        float intensity = intersectionObject->mSpecular;
        if (isTotalInternalReflection) {
            intensity += intersectionObject->mTransmission;
        }
        color = add(
            color,
            multiply(reflectionColor, intensity)
        );
    }

    return truncate(color, 1);
}

If the object is transparent then it computes the direction of the transmission ray and recursively traces it, unless the refraction causes total internal reflection. In that case, the transmission component is added to the reflection component and thus the color will be 100% of the traced reflection color.

I add a little bias to the intersection point in the direction of the normal (inverted if inside) when recursively tracing the transmission ray. If I don't do that, then I get this result:

Result without bias on intersection point

The computation for the direction of the transmission ray is performed in refractionDir. This function assumes that we will not have a transparent object inside another, and that the outside material is air, with a coefficient of 1.

Vec3f refractionDir(Vec3f ray, Vec3f normal, float refractionIndex, bool &isTotalInternalReflection) {
    float relativeIndexOfRefraction = 1.0f / refractionIndex;
    float cosi = -dot(ray, normal);
    if (isInside(ray, normal)) {
        // We should be reflecting across a normal inside the object, so
        // re-orient the normal to be inside.
        normal = multiply(normal, -1);
        relativeIndexOfRefraction = refractionIndex;
        cosi *= -1;
    }
    assert(cosi > 0);

    float base = (
        1 - (relativeIndexOfRefraction * relativeIndexOfRefraction) *
        (1 - cosi * cosi)
    );
    if (base < 0) {
        isTotalInternalReflection = true;
        return ray;
    }

    return add(
        multiply(ray, relativeIndexOfRefraction),
        multiply(normal, relativeIndexOfRefraction * cosi - sqrtf(base))
    );
}

Here's the result when the spheres are further away from the camera:

Spheres further away from camera

And closer to the camera:

Spheres closer to the camera


Edit: I noticed a couple bugs in my code.

When I add bias to the intersection point, it should be in the same direction as the transmission. I was adding it in the wrong direction by adding negative bias when inside the sphere. This doesn't make sense as when the ray is coming from inside the sphere, it will transmit outside the sphere (when TIR is avoided).

Old code:

add(intersection, multiply(normal, bias))

New code:

add(intersection, multiply(transmissionDirection, 1e-4))

Similarly, the normal that refractionDir receives is the surface normal pointing away from the center of the sphere. The normal I want to use when computing the transmission direction is one pointing outside if the transmission ray is going to go outside the object, or inside if the transmission ray is going to go inside the object. Thus, the surface normal pointing out of the sphere should be inverted if we're entering the sphere, thus is the ray is outside.

New code:

Vec3f refractionDir(Vec3f ray, Vec3f normal, float refractionIndex, bool &isTotalInternalReflection) {
    float relativeIndexOfRefraction;
    float cosi = -dot(ray, normal);
    if (isInside(ray, normal)) {
        relativeIndexOfRefraction = refractionIndex;
        cosi *= -1;
    } else {
        relativeIndexOfRefraction = 1.0f / refractionIndex;
        normal = multiply(normal, -1);
    }
    assert(cosi > 0);

    float base = (
        1 - (relativeIndexOfRefraction * relativeIndexOfRefraction) * (1 - cosi * cosi)
    );
    if (base < 0) {
        isTotalInternalReflection = true;
        return ray;
    }

    return add(
        multiply(ray, relativeIndexOfRefraction),
        multiply(normal, sqrtf(base) - relativeIndexOfRefraction * cosi)
    );
}

However, this all still gives me an unexpected result:

New result with proper normal handling

I've also added some unit tests. They pass the following:

  • A ray entering the center of the sphere parallel with the normal will transmit through the sphere without being bent (this tests two refractionDir calls, one outside and one inside).
  • Refraction at 45 degrees from the normal through a glass slab will bend inside the slab by 15 degrees towards the normal, away from the original ray direction. Its direction when it exits the sphere will be the original ray direction.
  • Similar test at 75 degrees.
  • Ensuring that total internal reflection happens when a ray is coming from inside the object and is at 45 degrees or wider.

I'll include one of the unit tests here and you can find the rest at this gist.

TEST_CASE("Refraction at 75 degrees from normal through glass slab") {
    Vec3f rayDirection = normalize(Vec3f({ 0, -sinf(5.0f * M_PI / 12.0f), -cosf(5.0f * M_PI / 12.0f) }));
    Vec3f normal({ 0, 0, 1 });
    bool isTotalInternalReflection;
    Vec3f refraction = refractionDir(rayDirection, normal, 1.5f, isTotalInternalReflection);
    REQUIRE(refraction[0] == 0);
    REQUIRE(refraction[1] == Approx(-sinf(40.0f * M_PI / 180.0f)).margin(0.03f));
    REQUIRE(refraction[2] == Approx(-cosf(40.0f * M_PI / 180.0f)).margin(0.03f));
    REQUIRE(!isTotalInternalReflection);

    refraction = refractionDir(refraction, multiply(normal, -1), 1.5f, isTotalInternalReflection);
    REQUIRE(refraction[0] == Approx(rayDirection[0]));
    REQUIRE(refraction[1] == Approx(rayDirection[1]));
    REQUIRE(refraction[2] == Approx(rayDirection[2]));
    REQUIRE(!isTotalInternalReflection);
}
Emily Horsman
  • 195
  • 1
  • 7
  • these things are realy hard to debug ... if you can add a debug draw (render one ray) to check how it is reflecting and refracting that usually shines some light onto the issues like this. Here is How mine debug draw looks like: [GLSL raytrace through 3D mesh](https://stackoverflow.com/a/45140313/2521214) Its the 3th image from top. I can switch to/from debug view during run time by a key press ... that way I can also check if the geometry is OK (not shifted/scaled/distorted) – Spektre Nov 30 '18 at 19:03
  • I've gone through the same issues few years back when I implemented a Ray Tracing renderer for a 3d App available here https://github.com/codetiger/Iyan3d/tree/master/iyan3d/trunk/Iyan3D-Ubuntu/src/SGRenderer. One easy bug I can think of is, starting a new ray from the point of intersection. The best practice is to move the starting point of the new Ray to slightly towards the direction of the ray so that it does not reflect on the surface again. – codetiger Jan 04 '19 at 09:38
  • The exact function that calculates the starting direction of a Ray is in the link below. https://github.com/codetiger/Iyan3d/blob/master/iyan3d/trunk/Iyan3D-Ubuntu/src/SGRenderer/common.h#L194 – codetiger Jan 04 '19 at 09:46
  • *The best practice is to move the starting point of the new Ray to slightly towards the direction of the ray so that it does not reflect on the surface again.* You're right! That's one issue I just found out 10 miniutes ago. – zwcloud Mar 23 '19 at 14:23
  • But the result is still black because after it bounced 5 times (max depth reached), the ray still doesn't reach the light source, for 4096 samples per pixel. That's unexpected. I'm still debugging. It's path tracing, not ray tracing, BTW. – zwcloud Mar 23 '19 at 14:25
  • @codetiger Increased max depth from 5 to 10. Now it is not black! See https://github.com/zwcloud/CSharpPathTracer/tree/9d632092063e6a5e19cfe098de5c5fa3b8ae7ac5 – zwcloud Mar 24 '19 at 10:32
  • @zwcloud Thanks for the update. I missed that point, I had come across max-depth issue as well. Meanwhile, in one of my implementations, I remember, making the max depth bit different (increased) for refracted & reflected rays. That will help in terms of performance. – codetiger Mar 25 '19 at 07:50

0 Answers0