Path Traced transparent shadows should be upgraded (with examples)

I’ve been using the pathtracer in unity for over two years now and I’ve found it to work well enough, but for it to suit my needs I had to modify it a bit.
First modification was to adress a shadowing bug and the second modification was for the russian roulette rules.
Now however I’ve noticed how lacking the way transparent shadows are handled by the pathtracer.
All transparent shadows are handled kindof as a single shadow without any form of thickness to them.
This makes sense since in the code all that the shadow consists of are two values,
surfaceData.transmittanceMask and surfaceData.transmittanceColor. These two values are multiplied together to form the resulting shadow.
As a result the shadows seem basic, and when the opacity is set to 0 (pure refraction) the shadows more or less disappear.
Because of this I went in and wanted to upgrade the shadows somehow.

Here are the default shadows, notice no shadows for the clear glass material and the basic colored shadows for the colored materials.

The first upgrade I did was to actually calculate a fresnel for the shadows. See the defined shadows for the sphere, box and Blender Suzanne model.

But that was using a basic fresnel, a simple dot product of the surface normal and the ray direction.
Doing it properly would be to use the same Schlick fresnel used by the lit shader itself. So I went ahead and added that.

and here in the sun instead of a pointlight

Not sure where those consentrations of light (fake caustics) are coming from on the Blender Suzanne model. Most likely an issue with the way I did the Schlick’s approximation. There’s also a problem with flat planes where their shadows are opaque with the Schlick fresnel.
The basic fresnel approach has no issues as far as I’ve managed to see.

I changed the transmission shadows code from

payload.value *= surfaceData.transmittanceMask * surfaceData.transmittanceColor;

to

float metalSmooth = ((surfaceData.perceptualSmoothness * 0.5) + 0.5) * (1 - surfaceData.metallic);
float3 tColor = exp(-TransmittanceColorAtDistanceToAbsorption(surfaceData.transmittanceColor, surfaceData.atDistance));
payload.value *= surfaceData.transmittanceMask * tColor * basicFresnel * metalSmooth;```

and for the Schlick fresnel I did this instead

```float metalSmooth = ((surfaceData.perceptualSmoothness * 0.5) + 0.5) * (1 - surfaceData.metallic);
float3 tColor = exp(-TransmittanceColorAtDistanceToAbsorption(surfaceData.transmittanceColor, surfaceData.atDistance));
float schlickFresnel = F_Schlick(IorToFresnel0(surfaceData.ior), dot(surfaceData.normalWS, WorldRayDirection()));
payload.value *= surfaceData.transmittanceMask * tColor * schlickFresnel * metalSmooth;```

What do you guys think?

I don’t use path tracing, but the difference is so great that Unity should immediately tweak the code to match, in my humble opinion.

1 Like

It indeed looks very nice but here’s my two cent about this :
This should maybe be applied to raytraced tinted shadows, not path-traced.
Adding a fresnel or Schlick fresnel on the shadow effet is doing a “fakery” on the effect, and could be a good way to increase the visuals for real time rendering.
But path-tracing goal is to be as close to real life and physical light as possible. The proper way to do this in path tracing would be a proper caustics implementation, some experimentations have been done internally with the current state of the path tracer, it kind of worked but required way to many samples and accumulation time to lead to usable results.

As it stand the shadows from pathtracing isn’t taking into account the thickness of the transparent surface (which this post is an approximation of), meaning you get even less realistic shadows than faking it.
I was trying to implement thickness being taken into account, but ended up just using fresnel on top of the regular transparent shadows.
It’s an approximation, but a more accurate one compared to not doing it at all.
My schlick implementation was done wrong but the result is interesting. The regular boring fresnel is not physically accurate, but waaaaay more accurate than treating the shadow as a tinted opaque shadow. In fact it’s probably fairly close to a proper transparent shadow as it takes the obliquity of the surface into account. The more oblique the angle the less light would go through the surface, making a shadow obviously.
The more accurate “approximation” would be the schlick fresnel, which I bungled :p.

The surfacedata has the data pertaining to the absorption distance from the material, but I didn’t find a way to use it properly to create “thick” shadows. If that is implemented the shadow would darken when the absorption distance is smaller. As an example, the Suzanne model would have a more defined, darker and tinted shadow for the head but a less so for the ears.
In theory it should be fairly simple, use the rays position with the distance to darken the shadow.

Anyway, the bottom line is that the way transparent shadows (especially ones with refraction) look increadibly fake and need an upgrade, however that’s done as this was just an example.

Some notes on how the shadows are handled currently.

  • At the moment metalness doesn’t affect the shadow, and that is a mistake. Since the metalness affects how opaque the surface appears even if it’s transparent, logically it should also darken the shadow.
  • Smoothness doesn’t affect the shadow either, but will affect the surface. Logically a rougher surface should cast a darker shadow than a smoother surface.

this looks amazing! i’m stealing this for my lightmapper.

i actually tried to do something very similar to create fake shadow penumbrae, and it caused leaks but with transparent surfaces it doesn’t matter!

kudos to you for figuring this one out. :smile:

1 Like

The path tracer is unbiased, that means the result is correct for what it’s supposed to do in the current configuration. Your solution is an approximation and introduces bias, which means it is less correct.

If you want transmission/scattering, you need to use a BRDF that supports it, by either selecting a more complex base shader on your material, or enabling the required material flags on your current material.

I do like your approximation though, and like @Remy_Unity said, this might actually be a nice addition for real time ray tracing (which is biased, it uses a lot of approximation).

The pathtracer being unbiased is irrelevant really, the shadow calculation is wrong after all. It’s just using transmittanceMask and transmittanceColor, which might I add just is just the opacity and the transmission color.
I don’t think doing it with the approximations I made is the way to do obviously, but it’s better than the way it’s done now unbiased or not.
Doing it properly would be to take the rays travel through the object into account.
And as it stands there’s no way of doing that with the pathtracer.

I’ve read through the different views here, and my uninformed opinion is that I agree with what you’re saying. Or at least, I lean toward this side of the fence.

After all, everything in graphics/rendering is at the end of the day, a model or approximation. Even our entire understanding of physics is just a rough model.

So if the stated goal of path tracing is realism, then it has to be said that the original image looks nothing like real life, while the upgraded image looks a lot more like real life.

Now since this visual upgrade might not be seen as pure and correct path tracing… just give it another name, like “hybrid path tracing” or “path tracing with fakery”. The end result should be that someone utilizing path tracing has a cheap means of augmenting those missing features that would be too expensive to do with true/pure path tracing.

From a Unity UI point of view, such features might be clearly marked as optional fakery.

The final visual result should in most cases take precedence over purity and terminology.

1 Like

All of this is wrong. Just like I wrote in my previous post, if you want your material to have transmission, then you need to enable transmission (for example, translucence) on the material.

Here are some quick renders in HDRP using the default Lit shader:

I think you’ve misunderstood the point of this post entirely.

Lightning with HDRIs is not the point of this post here at all. We’re talking about the direct light sources like point light, spot lights and directional lights.
That’s the part that has the wrong shadow calculation because it’s a separate calculation entirely from the regular light transport. Fairly certain we’re using NEE or something like that.
That part is calculating the shadows by using transmittanceMask and transmittanceColor like I said.
Which I will say again is just the opacity and the transmission color.
IOR, normal map, smoothness, metalness, transmittance absorption distance, and thickness from the material are not taken into account for that calculation.

Haha yes, everything at the end of the day is an approximation. This especially when we’re not doing anything even remotely close to spectral rendering, but even those are appropriations (a better approximation but still approximation).

Renaming and deviating from what the pathtracer is about?
Nah, not sure they need to go that far. All I’m suggesting is for them to upgrade the shadow calculation, however they go about it.

Nobody said anything about HDRIs, the above examples are lit by emissives/lights.

Here is the scene lit by a single point light.

There is no “shadow calculation”, path tracers don’t have shadows.
They are simply areas where the light does not reach, and by enabling transmission, you are allowing light to transmit through surfaces and reach where it normally wouldn’t. (and will brighten/color your observed “shadow”)

NEE does not impact any of this, it is a sampling strategy, it just changes how fast the image will converge to the exact same result.

Here is the same material with 3 different absorption distances, lit by a directional light.

1 absorption distance with varying smoothness, directional light

10 absorption distance with varying smoothness, directional light

Which version are you using?
Also, I’ve literally looked at the code, and there is parts dedicated to shadows for lights.

This is 2023.2 with HDRP16, I can prepare this scene in a small project for you if you want (or any other Unity / HDRP version, if that’s easier).

The part of the code you’ve seen calculates the light contribution (from SampleLights, the light list) and then multiplies the contribution (direct only, first depth transmission) of that light by the “shadow dimmer” value from the light component. If you want a good render, don’t use that option, it’s a hack.

That “hack” is what I’ve been referring to. For a speedy render it’s the only option, and by speedy I mean 50spp (or even less). Using emissive surfaces are not an option, it’s even less an option when dealing with hdr.

I disagree that it doesn’t produce good looking renders, but its shadows have been neglected for a while.
It is fixable though, I think you guys will probably find some way of doing it.