Color Shadows with Physically Based Rendering

Started by
9 comments, last by RPTD 11 years, 3 months ago

I've implemented PBR for my game engine. Now I'm somehow stuck on the topic of colored shadows. I've got the following test situation:

trasha.jpg

A simple spot light shines through a transparenc object, in this case a colored glass. Now the light has to be colored while passing through the colored glass. So far this is logic since the colored glass absorbs certain wavelength from the white light leaving behind colored light. I've got though some problems with certain concepts. Maybe somebody can help me out there.

The glass can have different levels of transparency. Let's say 0%, 25%, 50%, 75% or 100%. Obviously with 100% the light should be black since no light can pass through an opaque object and for 0% the light is white as the object is fully transparent. For the other cases though the light should be more or less strongly colored.

But what exactly is the physical basis behind this?

If 0% transparent the color should be white. For 100% transparent the color should be black. How does the transparency level mixing with the light color? Mix against white or black or both?

With PBR the lighting should sum up to one. The reflection and refraction part is governed by Fresnel so that sums up to one. This leaves the refraction part as the part to affect colored shadows (the amount of light refracted/transmitted). But how does the transparency correctly factor in this from a physical point of view?

I googled around already but could not really find a satisfying physical explenation of the problem. I know Blender(Cycles) works with transparency, refraction and transmission so I assume there's something I didn't fully understand yet.

Life's like a Hydra... cut off one problem just to have two more popping out.
Leader and Coder: Project Epsylon | Drag[en]gine Game Engine

Advertisement

For some light color C shining through a surface with transparency level t [0,1] and color S at the intersection point, I would expect the light going out the other end is (C * t * S), and the light reflected by the surface is C * (1-t) (of course split that into diffuse/specular/whatever makes sense for your light model)

Does this answer your question?

That is, unless you're trying to model some particular transparent material that has thickness and reflection / scattering properties, in which case this is much more complex. I didn't catch if this was an offline renderer or something you're trying to do in real time.

You have the right idea but not the right formula. Basically, after you apply the Fresnel equations to work out how much of the ray makes it across the medium boundary (and how much reflects off) the intensity of the ray will decrease exponentially with distance travelled (not linearly). If your medium (here, glass) has an extinction coefficient of k, the initial ray intensity is I0, and d is the distance travelled by the ray inside the medium, then:

[eqn]I_d = I_0 e^{-kd}[/eqn]

And this is the ray intensity at a distance d. This is assuming your medium is 100% homogeneous, with no scattering occurring inside. k = 0 means the medium does not absorb any light, this is physically impossible for any medium other than the vacuum, and k = infinity means the object is completely opaque. For clear glass, k will be pretty small, since light travels well inside. For a more opaque glass, it'll be higher, and so on..

Note that k is wavelength-dependent, if you are rendering in RGB you'll need three different extinction coefficients, kR, kG and kB.

See the Beer-Lambert law, and my last article has some words on absorption (among other stuff) you might find useful. This can be implemented in real-time and is a very minor change to most renderers, both realtime and offline.

“If I understand the standard right it is legal and safe to do this but the resulting value could be anything.”

That is, unless you're trying to model some particular transparent material that has thickness and reflection / scattering properties, in which case this is much more complex. I didn't catch if this was an offline renderer or something you're trying to do in real time.
Real-time rendering in a game engine.
You have the right idea but not the right formula. Basically, after you apply the Fresnel equations to work out how much of the ray makes it across the medium boundary (and how much reflects off) the intensity of the ray will decrease exponentially with distance travelled (not linearly). If your medium (here, glass) has an extinction coefficient of k, the initial ray intensity is I0, and d is the distance travelled by the ray inside the medium, then:

[eqn]I_d = I_0 e^{-kd}[/eqn]

And this is the ray intensity at a distance d. This is assuming your medium is 100% homogeneous, with no scattering occurring inside. k = 0 means the medium does not absorb any light, this is physically impossible for any medium other than the vacuum, and k = infinity means the object is completely opaque. For clear glass, k will be pretty small, since light travels well inside. For a more opaque glass, it'll be higher, and so on..

Note that k is wavelength-dependent, if you are rendering in RGB you'll need three different extinction coefficients, kR, kG and kB.

See the Beer-Lambert law, and my last article has some words on absorption (among other stuff) you might find useful. This can be implemented in real-time and is a very minor change to most renderers, both realtime and offline.
Okay, let's see if I can follow. I'm working with a PBR system hence I have Surface Reflection and SubSurface Reflection. Fresnel mixes between the two of them. As mentioned the Surface reflection is already taken care of. So this leaves us with SubSurface Reflection. For solid materials the SubSurface reflection absorbs certain wavelengths and reflects what we can call albedo or surface color (diffuse reflection). For transparent materials this would now split up into two components, the SubSurface Reflection as we had but additionally transmission. If I get this correctly the transparency material property sort of describes the mix between SubSurface Reflection and Transmission. This would make sense to me since for 100% transparency there is 100% SubSurface Reflection and 0% Transmission while for 0% transparency there is 0% SubSurface Reflection and 100% Transmission (if we neglect the bending of the light ray due to Snell's Law for a minute).

So this would mean the transmitted color would be (1-transparency)*albedo . Combined with already colored light this would then end up as:
lightColor * ( 1 - transparency ) * albedo .

Let's say transparency is 0% hence fully transparent. In this case the color of the light ray travelling through the material would be:
lightColor * ( 1 - 0 ) * albedo = lightColor * 1 * albedo = lightColor * albedo.
This means a light ray travelling through a fully transparent object is fully colored by the object just that 0% of this colored light ends up in the shadow map. So adding all this together this would yield:
fragmentInShadow = [ lightColor * ( 1 - transparency ) * albedo ] * transparency

Any mistake in that one?

Life's like a Hydra... cut off one problem just to have two more popping out.
Leader and Coder: Project Epsylon | Drag[en]gine Game Engine

Yes, but it does not make sense physically. See, light loses intensity when travelling inside a medium, at an exponential rate, and you are not taking this into account, which would probably result in transparent color appearing unnaturally clear or opaque. If you want to stay in the physically based realm, you're going to have to take distance travelled into account, otherwise this is just some approximation of transparency. I'm not sure what your pipeline looks like, since it's real-time, but this should not be too hard to integrate.

A stupid example, but with your current code, a light ray going through 1cm of glass would exit with exactly the same intensity as if it had been going through 10000km of glass, since distance travelled isn't used anywhere. This does not happen in practice! In real life, that light ray would've gotten completely absorbed a few dozen meters in.

“If I understand the standard right it is legal and safe to do this but the resulting value could be anything.”

That's correct. But in the case of 1cm the transparency map would contain a low value (let's say <25%) while with 10000km it would be 100% (opaque). The thickness is definitely the physically correct way to look at it but for an artist it is an unnatural material property that's difficult to handle and tune. I'm looking to understand the physics behind the problem and then to derive a PBR material property that is useful to the artist and clear to module programmers. I did the same for the surface roughness which I defined in a linear range instead of the typical exponential range as this is a lot more natural and predictable to work with while still allowing the module programmer to map it to the appropriate physical calculation under the hood.

For the transparency I want to have the same. I want the transparency to be defined in a linear way that is artist friendly while mapping to the physical representation where required. After all the absorbtion for a surface is calculated sooner or later into a transparency/coverage factor. Think of it as the final factor affecting the light color in respect to the surface color. So unless I misunderstood you the absorption leads directly to a transparency/coverage value in the range from [0..1] for the three major wavelength like transparency(rgb) = functionOfAbsorption(rgb). Since real-time rendering is anyways one approximitation stringed to the next I'm not that much concerned with a 100% accurate physical formula as this is anyways impossible. Important for me is to answer properly the question "if my colored material has a transparency of 25% (no matter what k and d value is actually required for this result using the absorption formula) how does the transmitted light ray look like?".

I'm operating here only in the flat surface situation where I have no knowledge about volume. Working for a true volume obviously is the next step like fog or liquids. That's though a different problem since there I can determine the distance and then absorption is useful and artist friendly. For a flat surface though it makes no sense and stuff like glass is typically rendered as a double sided triangle with mathetically infitesimally small thickness.

Life's like a Hydra... cut off one problem just to have two more popping out.
Leader and Coder: Project Epsylon | Drag[en]gine Game Engine

For the transparency I want to have the same. I want the transparency to be defined in a linear way that is artist friendly while mapping to the physical representation where required. After all the absorbtion for a surface is calculated sooner or later into a transparency/coverage factor. Think of it as the final factor affecting the light color in respect to the surface color. So unless I misunderstood you the absorption leads directly to a transparency/coverage value in the range from [0..1] for the three major wavelength like transparency(rgb) = functionOfAbsorption(rgb).

I understand, but for surface roughness, your linear range maps directly and unambiguously to exponential range, which means the result is still correct even though it's presented in a form easy to tweak by artists. But in this case, you are completely ignoring an entire dimension of the problem, distance, so what I can see is two possibilities:

- make your material's transparency depend on the thickness, which means the material is now dependent on the mesh

- convert the exponential "extinction coefficient" to a linear form which would be more useful to artists, but which will increase with distance

Otherwise, your artist-friendly value cannot and will not map to a proper absorption coefficient. In any case, your question of "what is the physical basis behind this" has been answered - there is none. However, as it is clear you do not want to do this and would rather use an approximation based on % transparency, I will stop here.

I'm operating here only in the flat surface situation where I have no knowledge about volume. Working for a true volume obviously is the next step like fog or liquids. That's though a different problem since there I can determine the distance and then absorption is useful and artist friendly. For a flat surface though it makes no sense and stuff like glass is typically rendered as a double sided triangle with mathetically infitesimally small thickness.

Ah, well that explains everything, I was under the impression you were working with volumes from the start. In that case, your incident light ray (after subsurface reflection) will just be multiplied with the transparency coefficient, for instance 0.25, which means 25% of the light makes it through the glass, and 75% is absorbed by it, and ends up in the shadow map as "absorbed light".

If your colored material has a transparency of 25%, then the transmitted ray has intensity 25% of the incident ray (after subsurface reflection), so multiplied by 0.25 (assuming that transparency applies for R, G and B - otherwise, multiply each channel as needed by the transparency coefficient). And 1 - 0.25 = 0.75 of the light ends up in the shadow map, as having been absorbed by the medium.

So, when the light hits the surface, it gets reflected according to the fresnel equations. Then, it goes on to subsurface reflection, and gets modulated by albedo:

lightColor * albedo -> this is the color that gets reflected from subsurface

Now, whatever light is left will be going through the glass and exiting at the other end (ignoring the possibility that it may get reflected back into the glass), this is where transparency comes in:

lightColor * albedo * transparency

And the shadow map is then equal to:

lightColor * albedo * (1 - transparency)

At least that's what I would expect. Can you detail exactly what all your parameters are, and what happens when light hits a glass triangle for instance?

“If I understand the standard right it is legal and safe to do this but the resulting value could be anything.”

I'm creating for transparent shadow casters in addition to the depth map a color map. That's an RGBA texture. RGB stores the color a white light ray would have after passing the material. A stores the transparency or rather said the intensity of the light ray after passing the material. A=0 leaves the light ray unaltered while A=1 would fully block the light ray. During lighting there are some additional values but they are not important here. So for a fragment lit using this way the parameters sum up like this:

lightColor.rgb := the color of the light source for the red, green and blue component

shadowColor.rgb := the color of the transparent shadow caster (what's left of the light after passing the material)

shadowColor.a := the transparency of the transparent shadow caster (what fraction of light intensity is left after passing the material)

So using these values the following invariants have to be fulfilled:

1) shadowColor.a = 0: material is fully transparent. shadowedLightColor.rgb = lightColor

2) shadowColor.a = 1: material is fully opaque. shadowLightColor.rgb = black

Using your version I end up with this:

shadowLightColor.rgb = lightColor.rgb * shadowColor.rgb * ( 1 - shadowColor.a )

invariant (2) is fulfilled but invariant (1) is violated:

shadowLightColor.rgb = lightColor.rgb * shadowColor.rgb * ( 1 - 0 ) = lightColor.rgb * shadowColor.rgb // fail

but it should be

shadowLightColor.rgb = lightColor.rgb

I made the following modification to fulfill invariant (1):

shadowLightColor.rgb = lightColor.rgb * mix( white, shadowColor.rgb, shadowColor.a ) * ( 1 - shadowColor.a )

This fulfills now both invariants:

(1) shadowLightColor.rgb = lightColor.rgb * mix( white, shadowColor.rgb, 0 ) * ( 1 - 0 ) = lightColor.rgb * white = lightColor.rgb // q.e.d.

(2) shadowLightColor.rgb = lightColor.rgb * mix( white, shadowColor.rgb, 1 ) * ( 1 - 1 ) = lightColor.rgb * shadowColor.rgb * 0 = black // q.e.d.

So the shadowColor has to be pulled towards white to fulfull the invariants. This fudge factor though doesn't make me happy as it doesn't have a physical something backing it up but it does satisfy the invariants. I have the feeling something is wrong here but I can't put my finger on it.

Life's like a Hydra... cut off one problem just to have two more popping out.
Leader and Coder: Project Epsylon | Drag[en]gine Game Engine

isnt the transparency (shadowColor.a) information already contained in shadowColor.rgb?

i mean, what happens is you have shadowColor.rgb = (0,0,0) and shadowColor.a = 0?:

shadowColor.rgb := the color of the transparent shadow caster (what's left of the light after passing the material)

=> since this is 0,0,0 there is NO light after passing the material (or what am i missing?) => the object seems to be be opaque, (definition of NO light passing) which contradicts:

1) shadowColor.a = 0: material is fully transparent.

mindboggling! :)

i realize that you want a system in which an object has a transparency property (shadowColor.a) which acts regardless of color of the object (e.g. if alpha = 0 the color of caster becomes irrelevant, for alpha = 1 the color of light becomes irrelevant)

transparency is 'encoded' in shadowColor.rgb and would be approximated simply by something like lightColor.rgb-shadowFilter.rgb, i.e. the light gets either absorbed, or it doesnt. how much gets absorbed is determined by thickness, and the value of shadowColor.rgb, which is a material specific property

a caster fully transparent to all colors has the value (0,0,0), and fully opaque (1,1,1). in reality transmission of light is calculated by adding absorption and reflection (called extinction iirc) and subtracting that from incident light. you get more terms to this if you consider refraction at boundaries between media or within a medium like scattering and aberration (or some far out stuff like recombination of electron set into an exicited state by the incident light) most of which would contribute minimally to visual quality in realistic real time rendering

This topic is closed to new replies.

Advertisement