# Shading normal and geometric normal for refractive surface rendering

I got confused when implementing my own renderer. I read this from the PBR-book:

Fortunately, there is an elegant solution to these problems. When evaluating the BSDF, we can use the geometric normal to decide if we should be evaluating reflection or transmission: if $$w_i$$ and $$w_o$$ lie in the same hemisphere with respect to $$n_g$$, we evaluate the BRDFs, and otherwise we evaluate the BTDFs. In evaluating the scattering equation, however, the dot product of the normal and the incident direction is still taken with the shading normal rather than the geometric normal.

But when it comes to the calculation of refraction, I just can't find myself convinced, about when to use $$n_g$$ and when to use $$n_s$$. Here is thing, if I have an object like this: and when the light beam tries to enter the medium from the outside, I need to know:

• Whether I should have reflection or refraction (depending on sampling the Fresnel term)
• The IOR for outside and the inside of the interface, which is confusing since normally I use the dot product of ray direction and geometric normal to determine whether the ray points inwards or outwards.However, when geometric normal tells me the ray points inwards, the shading normal tells me otherwise, so I can't actually know the IOR for both sides.

I don't quite see how this corresponds to things said in the PBR-book, since if we decide to have a transmission event according to $$n_g$$, the final calculate according to $$n_s$$ might make it look like it's having a reflection event... Any help would be much appreciated, I got totally confused here.

Now it looks like I have to try to figure it out myself... since I've waited for days without an answer. So I decided to dig into the code of pbrt-v3 and I found what I need:

Spectrum BSDF::Sample_f: first we convert the world direction vector into the local frame. This can be found in line 714 of src/core/reflection.cpp:

Vector3f wi, wo = WorldToLocal(woWorld);

// and the WorldToLocal function just uses shading normal:
// This can be found in line 178 of src/core/reflection.h:
Vector3f WorldToLocal(const Vector3f &v) const {
return Vector3f(Dot(v, ss), Dot(v, ts), Dot(v, ns));
}


And then, we start to sample the BSDF. We take the typical BSDF used in glass material FresnelSpecular for an example (refer to line 487 of src/core/reflection.cpp).

Spectrum f = bxdf->Sample_f(wo, &wi, uRemapped, pdf, sampledType);

• First, sampling according to Fresnel's law, and we can see that we are using shading normal here:

Float F = FrDielectric(CosTheta(wo), etaA, etaB);


since wo is converted to local frame (defined by shading normal)

• Reflection sampling has nothing to do with ROI therefore we just use shading normal to calculate reflection. This is kinda trivial but I think I need to get back to this later and explain this with the picture posted in my question since I think there are some nasty situations. So we skip the if branch and focus the else branch.

• Obviously, shading normal is used to decide the interior and exterior (ROI) for the medium:

bool entering = CosTheta(wo) > 0;
Float etaI = entering ? etaA : etaB;
Float etaT = entering ? etaB : etaA;


Actually, for specular reflection/transmission materials, we have no more thing to explain: line 767:

if (!(bxdf->type & BSDF_SPECULAR)) {


skip the rest of the f part and even if we need to use f, due to being specular, all we would get is zero anyway, so why bother? Therefore for the situation shown in the figure below, if we are using simple specular reflection & transmission material, maybe we only have to stick with $$n_s$$. But what about non-specular situation? For example, what if I use MicrofacetReflection / MicrofacetTransmission (the former requires no ROI, so I use this as a simple example, the latter does, though)? I present two examples below, and the logic we should pay attention to in pbrt-v3 is (starting from line 766):

// Compute value of BSDF for sampled direction
if (!(bxdf->type & BSDF_SPECULAR)) {
bool reflect = Dot(*wiWorld, ng) * Dot(woWorld, ng) > 0;
f = 0.;
for (int i = 0; i < nBxDFs; ++i)
if (bxdfs[i]->MatchesFlags(type) &&
((reflect && (bxdfs[i]->type & BSDF_REFLECTION)) ||
(!reflect && (bxdfs[i]->type & BSDF_TRANSMISSION))))
f += bxdfs[i]->f(wo, wi);
}

• Reflection example. This is weird, it has the conflicts I mentioned in the question: $$n_g$$ and $$n_s$$ propose differently. However, if I understand correctly, in this situation we have two directions on the different sides of the geometric surface ($$n_g$$), therefore bool reflect is false here. During the for loop reflect && (bxdfs[i]->type & BSDF_REFLECTION will be false  and !reflect && (bxdfs[i]->type & BSDF_TRANSMISSION) will be false again since reflect = false and MicrofacetReflection has type BSDF_REFLECTION... so f will actually be zero. I think it is reasonable, since we can't let the light leak beneath the geometric surface.

• Transmission example. We use MicrofacetTransmission and sampled a transmission event, great. However, bool reflect  here will be true since both directions are on the same side about $$n_g$$. Again, the for loop results in a zero, since there is again a miss match (BSDF has transmission type, we sampled a transmission according to $$n_s$$ but $$n_g$$ tells us we are having a reflection event). So the result is again invalidated, in case we mistook the same side as fictional interior and exterior. I have to admit that this example feels kinda weird to me..., it's just a feeling and I can't actually put it accurately, but basically I think this is the case.

To wrap up:

• Specular reflection & transmission: stick with $$n_s$$ will be okay.
• Non-specular: check if $$n_g$$ and $$n_s$$ agree with each other on which event should happen, and if there is a conflict, the spectrum should be made zero.

These are just my understanding, I don't know if these are correct or not. Maybe I'll just implement these in my own renderer for testing. Notify me if anything feels wrong.