22

I am currently working on a raytracer just for fun and I have trouble with the refraction handling.

The code source of the whole raytracer can be found on Github EDIT: The code migrated to Gitlab.

Here is an image of the render:

Refraction bug

The right sphere is set to have a refraction indice of 1.5 (glass).

On top of the refraction, I want to handle a "transparency" coefficient which is defined as such :

  • 0 --> Object is 100% opaque
  • 1 --> Object is 100% transparent (no trace of the original object's color)

This sphere has a transparency of 1.

Here is the code handling the refraction part. It can be found on github here.

Color handleTransparency(const Scene& scene,
                         const Ray& ray,
                         const IntersectionData& data,
                         uint8 depth)
{
  Ray refracted(RayType::Transparency, data.point, ray.getDirection());
  Float_t eta = data.material->getRefraction();

  if (eta != 1 && eta > Globals::Epsilon)
    refracted.setDirection(Tools::Refract(ray.getDirection(), data.normal, eta));
  refracted.setOrigin(data.point + Globals::Epsilon * refracted.getDirection());
  return inter(scene, refracted, depth + 1);
}

// http://graphics.stanford.edu/courses/cs148-10-summer/docs/2006--degreve--reflection_refraction.pdf
Float_t getFresnelReflectance(const IntersectionData& data, const Ray& ray)
{
  Float_t n = data.material->getRefraction();
  Float_t cosI = -Tools::DotProduct(ray.getDirection(), data.normal);
  Float_t sin2T = n * n * (Float_t(1.0) - cosI * cosI);

  if (sin2T > 1.0)
    return 1.0;

  using std::sqrt;
  Float_t cosT = sqrt(1.0 - sin2T);
  Float_t rPer = (n * cosI - cosT) / (n * cosI + cosT);
  Float_t rPar = (cosI - n * cosT) / (cosI + n * cosT);
  return (rPer * rPer + rPar * rPar) / Float_t(2.0);
}

Color handleReflectionAndRefraction(const Scene& scene,
                                    const Ray& ray,
                                    const IntersectionData& data,
                                    uint8 depth)
{
  bool hasReflexion = data.material->getReflexion() > Globals::Epsilon;
  bool hasTransparency = data.material->getTransparency() > Globals::Epsilon;

  if (!(hasReflexion || hasTransparency) || depth >= MAX_DEPTH)
    return 0;

  Float_t reflectance = data.material->getReflexion();
  Float_t transmittance = data.material->getTransparency();

  Color reflexion;
  Color transparency;

  if (hasReflexion && hasTransparency)
  {
    reflectance = getFresnelReflectance(data, ray);
    transmittance = 1.0 - reflectance;
  }

  if (hasReflexion)
    reflexion = handleReflection(scene, ray, data, depth) * reflectance;

  if (hasTransparency)
    transparency = handleTransparency(scene, ray, data, depth) * transmittance;

  return reflexion + transparency;
}

Tools::Refract is simply calling glm::refract internally. (So that I can change easily if I want)

I don't handle notions of n1 and n2: n2 is considered to always be 1 for air.

Am I mising something obvious ?


EDIT

After adding a way to know if a ray is inside an object (and negating the normal if so) I have this :

Refraction problem

While looking around to find help, I stumbled upon this post but I don't think the answer answers anything. By reading it, I don't understand what I'm supposed to do at all.


EDIT 2

I've tried a lot of things and I am currently at this point :

Current

It's better but I'm still not sure if it's right. I'm using this image as an inspiration :

Example

But this one is using two indexes of refraction (To be closer to reality) while I want to simplify and always consider air as the second (in or out) material.

What I essentially changed in my code is here :

inline Vec_t Refract(Vec_t v, const IntersectionData& data, Float_t eta)
{
  Float_t n = eta;

  if (data.isInside)
    n = 1.0 / n;
  double cosI = Tools::DotProduct(v, data.normal);

  return v * n - data.normal * (-cosI + n * cosI);
}

Here is another view of the same spheres :

Spheres

Telokis
  • 3,399
  • 14
  • 36
  • `if (eta != 1` does this promote eta to double or promote 1 to float? – huseyin tugrul buyukisik Feb 14 '17 at 05:53
  • I think this will promote 1 to float. (`Float_t` is `double`, by the way) – Telokis Feb 14 '17 at 05:55
  • are you sure `eta > Globals::Epsilon` is true from where you look at that black sphere? Maybe some rays can't enter the sphere and can't reach camera? Did you try looking from a orthogonal angle to light sources? You are doing backwards tracking raytrace are you? – huseyin tugrul buyukisik Feb 14 '17 at 06:01
  • Yes, `data.material->getRefraction()` returns a constant. This is a user-defined value giving the indice of refraction for the given object. (Its value is 1.5 in this specific test case) – Telokis Feb 14 '17 at 06:03
  • `transmittance = 1.0 - reflectance;` overrides `Float_t transmittance = data.material->getTransparency();` maybe? – huseyin tugrul buyukisik Feb 14 '17 at 06:07
  • That shouldn't happen because of the `if (hasReflexion` right above it – Telokis Feb 14 '17 at 06:08
  • `eta != 1` due to floating point imprecision and depending on how `eta` calculated, this may never be true. You could find yourself with nothing but 0.9999999999 or 1.000000001 for `eta`. – user4581301 Feb 14 '17 at 06:10
  • `eta` is not calculated, it is an user inputted value – Telokis Feb 14 '17 at 06:11
  • maybe `refracted.setOrigin(data.point + Globals::Epsilon * refracted.getDirection());` can't put new ray into sphere, its still outside maybe, so can't ever enter sphere, could you try with larger multiplier here please? – huseyin tugrul buyukisik Feb 14 '17 at 06:18
  • Just tried with `0.001` (Epsilon is really small like 0.0000001). The result is the exact same image. – Telokis Feb 14 '17 at 06:21
  • Shouldn't a refracted image on a sphere be upside down? Because on the picture You put here it seems it's not... – Dori Feb 17 '17 at 06:53
  • It depends on the indice of refraction. Whether it's above or below 1 determines the orientation of the result. But I don't know which one is which – Telokis Feb 17 '17 at 08:08
  • Another thing - do You do the intersection with the back side of the sphere? – Dori Feb 17 '17 at 17:54
  • Yes, I handle this thanks to my `isInside` property – Telokis Feb 17 '17 at 22:24
  • @Ninetainedo the image is flipped when the relative index of the object is more than 1. And to answer Matso's question, yes, the image should be flipped (index = 1.5) – Kunal Tyagi Feb 19 '17 at 03:09
  • @KunalTyagi Thanks for the clarification. That's just a `1 / eta` to change I think. – Telokis Feb 19 '17 at 04:12
  • @Ninetainedo `1 / eta` fixes the case for `eta > 1` but the black ring still persists when `eta < 1`. – Dori Feb 19 '17 at 13:08
  • The qestion is whether `eta` **can get** less the 1 in real life? – Dori Feb 20 '17 at 06:03
  • @cmaster `Tools::Refract is simply calling glm::refract internally.` I made some changes, I'm gonna update my post – Telokis Feb 20 '17 at 16:41
  • Ah, didn't see that. So we have to assume that's correct. Looking up the documentation, I find that `glm::refract()` returns zero for cases where refraction is impossible, leading to total reflection. This can only happen when the ray is trying to enter the less dense medium. I guess that explains the black ring: You are applying the `eta` parameter the wrong way round; you need to use `1/eta` instead. Nevertheless, the distortionless center is actually what would worry me most. Have you checked that your inputs to `glm::refract()` are actually both normalized vectors? – cmaster - reinstate monica Feb 20 '17 at 16:52
  • @cmaster Yes they are both normalized, I tried normalizing them again and it didn't change anything. Have you taken a look at the updated post ? – Telokis Feb 20 '17 at 17:41
  • Yes, I have. The distorted image with the 0.7 looks quite like a real glass sphere now :-) (which means that you definitely need to invert the `if()` condition in `Refract()` to switch the two cases). With your own formula you also ignore the total reflection case, which leads to the wrong behavior in the rim of the right sphere where the black ring had been when you were using `glm::refract()`. – cmaster - reinstate monica Feb 20 '17 at 18:53
  • Ok I inverted the if and [this](http://image.prntscr.com/image/0124c61a752a4b519264619780b7af8a.png) is what I get now. I guess it is more correct. I don't really understand what there is a wrong behavior on the right sphere. How am I supposed to handle total internal reflection ? – Telokis Feb 20 '17 at 20:21
  • I am having a similar problem with black borders, how did you get from start to the first EDIT? – Marcus May 09 '20 at 00:26
  • @Marcus I'm sorry but I don't remember, it's been quite a long time. I probably tweaked my refraction algorithm or changed the max depth for recursion. The github repository doesn't exist anymore because I migrated to Gitlab. If you want to take a look, the project is available here: https://gitlab.com/Telokis/Rayon – Telokis May 09 '20 at 09:15
  • @Telokis no worries. I'll look into your repository, thanks. – Marcus May 09 '20 at 11:15

2 Answers2

6

EDIT: I've figured that the previous version of this was not entirely correct so I edit the answer.

After reading all the comments, the new versions of the question and doing some experimentation myself I produced the following version of refract routine:

float3 refract(float3 i, float3 n, float eta)
{
    eta = 2.0f - eta;
    float cosi = dot(n, i);
    float3 o = (i * eta - n * (-cosi + eta * cosi));
    return o;
}

This time calling it does not require any additional operations:

float3 refr = refract(rayDirection, normal, refrIdx);

The only thing I am still not sure is the inverting of the refractive index when doing the inside ray intersection. In my test the produced image haven't differ much no matter I inverted the index or not.

Below some images with different indices:

enter image description here enter image description here

For more images see the link, because the site do not allow me to put more of them here.

Dori
  • 675
  • 1
  • 7
  • 26
  • Using your `refract` gives me [this](http://image.prntscr.com/image/52166780f62942398063b295a8c77297.png). The right image is your refract and the right one is mine. eta are 0.7 and 1.5 respectively for each sphere. – Telokis Feb 21 '17 at 21:39
  • Probably I do not handle the _inside_ refraction well. – Dori Feb 22 '17 at 05:31
  • @Ninetainedo And is it with inverting the index when doing the _inside_ refraction or without? – Dori Feb 22 '17 at 08:02
  • I don't invert the index but I invert the normal if I'm inside. – Telokis Feb 22 '17 at 17:37
  • @Ninetainedo So do You do the normal invert inside the `refract` routine? Or just pass the inverted normal to it? How does it looks like now? Is it exactly like the one You posted before in the last edit of the question? – Dori Feb 22 '17 at 17:52
  • With your routine, I didn't change anything, I used your exact function. And the result is what I linked in my comment right above. – Telokis Feb 22 '17 at 18:21
  • I've edited the routine because the `sgn` had no inpact on the outcome of it. Now it is exactly as the one You posted, but with the subtraction at the beginning. – Dori Feb 22 '17 at 18:57
5

I am answering this as a physicist rather than a programmer as haven't had time to read all the code so won't be giving the code to do the fix just the general idea.

From what you have said above the black ring is for when n_object is less than n_air. This is only usually true if you are inside an object say if you were inside water or the like but materials have been constructed with weird properties like that and it should be supported.

In this type of situation there are rays of light that can't be diffracted as the diffraction formula put the refracted ray on the SAME side of the interface between the materials, which obviously doesn't make sense as diffraction. In this situation the surface will instead act like it's a reflective surface. This is the situation that is often referred to as total internal reflection.

If being fully exact then almost ever refractive object will also partially reflective too and the fraction of light that is reflected or transmitted (and therefore refracted) is given by the Fresnel equations. For this case though it would still be a good approximation to just treat is as reflective if the angle is too far and transmitting (and therefore refractive) otherwise.

Also there are situations where this black ring effect can be seen if reflection is not possible (due to it being dark in those directions) but light that is transmitted being possible. This could be done by say taking a tube of card that fits tightly to the edge of the object and is pointed directly away and only shining light inside the tube not outside.

staircase27
  • 267
  • 3
  • 12