Simple GPU Path Tracing, Part 7.1 : Refractive material

 

So we now have a good set up for volumetric rendering in our path tracer. We have implemented a simple volume material with no surface. Now, we can implement another type of volumetric material that does have a surface, meaning the light can either reflect from it, or scatter inside.

 


Here's the commit for this post. 

We will again rely heavily on the famous paper by Walter et al. that we've already used before in our physically based BRDF implementation.

Let's create a new material type, we'll call it glass material, as it's a material that we will see through, but that still has a surface.

#define MATERIAL_TYPE_GLASS   3

And we'll set our small box to use this material : 

ShortBoxMaterial.MaterialType = MATERIAL_TYPE_GLASS;

We don't have to add other parameters to the material struct, as we can use all of the existing ones for describing how light reflects from a surface, and how it scatters into it.

So we can jump straight in our implementation!

in EvalMaterial, we'll have to calculate the density for glass materials too : 

if(Material.MaterialType == MATERIAL_TYPE_VOLUMETRIC || Material.MaterialType == MATERIAL_TYPE_GLASS)

we also have to change the IsVolumetric function to account for this new material :


FN_DECL bool IsVolumetric(INOUT(materialPoint) Material)
{
    return ( (Material.MaterialType == MATERIAL_TYPE_VOLUMETRIC) ||    
             (Material.MaterialType == MATERIAL_TYPE_GLASS));
}

Then, we'll have to write the 3 usual functions for new materials : SampleBSDFCos, SampleBSDFCosPDF, and EvalBSDFCos.

First, let's look at SampleGlass(). When we generate the next direction for the ray, we either want the ray to be reflected off the surface, or scattered inside. As we've seen before, the portion of light that is reflected is dictated by the fresnel equations. Here, we will not use the Schlick approximation anymore, but rather the dielectric Fresnel formulas : 

where n_1 and n_2 are the indices of refraction of the 2 materials (Typically, n1 is the air, and n2 is the material). the angle theta is calculated with Snell's law, using the refract function in glsl.

Here's the implementation :

FN_DECL float FresnelDielectric(float Eta, vec3 Normal, vec3 Outgoing)
{
    // The Fresnel equations describe how light is reflected and refracted 
    // at the interface between different media, 
    // such as the transition from air to a dielectric material.

    // https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/
    float CosW = abs(dot(Normal, Outgoing));
    float Sin2 = 1 - CosW * CosW;
    float Eta2 = Eta * Eta;

    float Cos2T = 1 - Sin2 / Eta2;
    if(Cos2T < 0) return 1;

    float T0 = sqrt(Cos2T);
    float T1 = Eta * T0;
    float T2 = Eta * CosW;

    float RS = (CosW - T1) / (CosW + T1);
    float RP = (T0 - T2) / (T0 + T2);

    return (RS * RS + RP * RP) / 2;
}

Now let's see how EvalGlass works : 

So we first check if we're entering the surface, by checking the angle between the normal of the surface and the outgoing direction. 

 

We see that if we're entering, o and n have an angle < 90deg, which means their dot product is > 1.       If the ray is going out, o and n have a  angle > 90deg, which means their dot product is < 1.

Then, depending on whether we're entering or not, we'll use the Fresnel function with the IOR of the surface, or its inverse if we're going out.

Then, based on the result of the Fresnel term, if the ray is reflected, we use the reflect() function of glsl. otherwise, we use the refract() function.

FN_DECL vec3 SampleGlass(vec3 Colour, float IOR, float Roughness, vec3 Normal, 
    vec3 Outgoing, float RNL, vec2 RN)
{
    bool Entering = dot(Normal, Outgoing) >= 0;
    vec3 UpNormal = Entering ? Normal : -Normal;
    vec3 Halfway = SampleMicrofacet(Roughness, UpNormal, RN);

    if(RNL < FresnelDielectric(Entering ? IOR : (1/IOR), Halfway, Outgoing))
    {
        vec3 Incoming = reflect(-Outgoing, Halfway);
        if(!SameHemisphere(UpNormal, Outgoing, Incoming)) return vec3(0);
        return Incoming;
    }
    else
    {
        vec3 Incoming = refract(-Outgoing, Halfway, Entering ? (1 / IOR) : IOR);
        if(SameHemisphere(UpNormal, Outgoing, Incoming)) return vec3(0);
        return Incoming;
    }
}


Cool, now we have the next ray direction after hitting a glass surface. Next, let's see how we compute the bsdf of such a surface. Again, there will be two distinct cases in the EvalGlass function.

the if statement checks if the ray is exiting from the surface (No matter if it's being reflected, or if the ray is exiting the surface after scattering inside). 

If the ray is entering / exiting the shape, we calculate the brdf as usual with the PBR equations.

Otherwise, the ray is being refracted inside the shape after hitting the surface, in which case we have to use the refraction term of the BSDF, which is explained in the  paper, equation 21 : 

That's quite a complicated equation ! But here's the implementation, not too bad :

FN_DECL vec3 EvalGlass(vec3 Colour, float IOR, float Roughness, vec3 Normal, vec3 Outgoing, vec3 Incoming)
{
    bool Entering = dot(Normal, Outgoing) >= 0;
    vec3 UpNormal = Entering ? Normal : -Normal;
    float RelIOR = Entering ? IOR : 1 / IOR;
   
    if(dot(Normal, Incoming) * dot(Normal, Outgoing) >=0)
    {
        vec3 Halfway = normalize(Incoming + Outgoing);
        float F = FresnelDielectric(RelIOR, Halfway, Outgoing);
        float D = MicrofacetDistribution(Roughness, UpNormal, Halfway);
        float G = MicrofacetShadowing(Roughness, UpNormal, Halfway, Outgoing, Incoming);
        return vec3(1) * F * D * G /
                abs(4 * dot(Normal, Outgoing) * dot(Normal, Incoming)) *
                abs(dot(Normal, Incoming));
    }
    else
    {
        vec3 Halfway = -normalize(RelIOR *  Incoming + Outgoing) * (Entering ? 1.0f : -1.0f);

        float F = FresnelDielectric(RelIOR, Halfway, Outgoing);
        float D = MicrofacetDistribution(Roughness, UpNormal, Halfway);
        float G = MicrofacetShadowing(Roughness, UpNormal, Halfway, Outgoing, Incoming);
       
        return vec3(1) *
                abs(
                    (dot(Outgoing, Halfway)*dot(Incoming, Halfway)) /
                    (dot(Outgoing, Normal) * dot(Incoming, Normal))
                ) * (1 - F) * D * G /
                pow(RelIOR * dot(Halfway, Incoming) + dot(Halfway, Outgoing), 2.0f) *
                abs(dot(Normal, Incoming));
    }
}

Okay, now what's left is the PDF calculation. The same condition applies here. If the ray is being reflected, we use the regular pbr pdf as before.

Otherwise, we'll use equation 17 from the same paper : 

 

FN_DECL float SampleGlassPDF(vec3 Colour, float IOR, float Roughness, vec3 Normal, vec3 Outgoing, vec3 Incoming)
{
    bool Entering = dot(Normal, Outgoing) >= 0;
    vec3 UpNormal = Entering ? Normal : -Normal;
    float RelIOR = Entering ? IOR : (1 / IOR);
    if(dot(Normal, Incoming) * dot(Normal, Outgoing) >= 0)
    {
        vec3 Halfway = normalize(Incoming + Outgoing);
        return FresnelDielectric(RelIOR, Halfway, Outgoing) *
               SampleMicrofacetPDF(Roughness, UpNormal, Halfway) /
               (4 * abs(dot(Outgoing, Halfway)));
    }
    else
    {
        vec3 Halfway = -normalize(RelIOR * Incoming + Outgoing) * (Entering ? 1.0f : -1.0f);
        return (1 - FresnelDielectric(RelIOR, Halfway, Outgoing)) *
               SampleMicrofacetPDF(Roughness, UpNormal, Halfway) *
               abs(dot(Halfway, Incoming)) /
               pow(RelIOR * dot(Halfway, Incoming) + dot(Halfway, Outgoing), 2.0f);
    }
}

And we're done, amazing!

Here are some renders with that material :

 

 


 Links

Next post : Simple GPU Path Tracing, Part 8 : Denoising

Commentaires

Articles les plus consultés