Page MenuHome

Cycles: add Principled Volume shader.
ClosedPublic

Authored by Brecht Van Lommel (brecht) on Jan 30 2018, 9:40 PM.

Details

Summary

Similar to the Principled BSDF, this should make it easier to set up volume materials. Smoke and fire can be rendererd with just a single principled volume node, the appropriate attributes will be automatically used when available.

  • Color: scattering color. Multiplied with "color" attribute if available.
  • Density: density of the volume. Multiplied with "density" attribute if available.
  • Anisotropy: Henyey-Greenstein scattering anisotropy.
  • Absorption Color: extra control over shadow/absorption color, separate from scattering color.
  • Emission: emission strength.
  • Emission Color: emission color modulation.
  • Blackbody: blackbody emission intensity, for rendering fire.
  • Blackbody Temperature: temperature in Kelvin. Affected by "flame" attribute if available.

There's still a bunch of TODO's, and no OSL or Eevee implementation, but it works well enough for early testing and feedback.

Diff Detail

Repository
rB Blender

Event Timeline

Test file with water, clouds and fire.

Add OSL implementation.

I'm not sure such an intrinsic multiplication by attributes is something really handy. It's fine for some simple effects, but makes it more difficult to combine simulated fire/smoke with extra procedural effects (our favorite example: tornado from Gooseberry). Or maybe you want to color-map or curve-map some attributes for an artistic choise (or to make simulation to work for a specific shot, without need to re-simulate).

Blackbody controls i didn't fully grasp currently. it seems you need to have non-zero Emission slider in order for blackbody to have effect, But then even at blackbody of 1 Emission Color is still having effect. Is it just adding blackbody color to emission color?

For me automatically using grids is a matter of usability, if rendering a mesh required adding an attribute node to read the geometry that would be pretty bad as well, and in a way this is similar. But we should indeed support manually reading and linking grids as well.

The blackbody color is multiplied by the emission strength and color, and I think that artistic control is useful. But we should have a better way to turn blackbody off/on I think.

There's a bunch of things the we should ideally support:

  • Homogeneous volume like mist or water.
  • Smoke + fire simulation with grids, manually linked or automatically used.
  • Emission from blackbody temperature, or something custom from density or another grid.
  • Use scatter color grid or not. It uses a lot of memory/time but most it's actually a fixed color.
  • Displacement of volume lookup position to add more detail.
  • In the future: arbitrary VDB grids or volume objects.

There's a few ways to make a UI for this, I'm not sure yet what would be best:

  • Booleans to enable/disable loading of certain grids.
  • Mode enum, with options like "Smoke", "Smoke + Fire", "Colored Smoke", "Custom", .. .
  • Sockets like "Density Grid" that if linked will override the automatically loaded one.

Further it would be good to make it easier to do manual setups, since it's actually quite complicated to do correctly now:

  • "color" is premultiplied by density, so if you're not careful the density will be used twice and the scatter albedo will be wrong.
  • "flame" does not have an obvious interpretation, remapping it from 0..1 to something like 1500..3000K gives a usable temperature but the exact meaning is unclear.
  • In VDB files, it is more common to have "temperature" grid where 0..1 maps to 0..max_temperature K. We could add an attribute that works like this.
  • The Blackbody node only gives normalized colors, we could add support for using the Stefan-Boltzmann law to generate the appropriate intensity.

There are also performance concerns. Ideally we should early out as fast as possible when ray marching through empty space, but SVM doesn't do much lazy evaluation so it's not so easy to avoid evaluating all nodes when density is 0.0.

  • "flame" does not have an obvious interpretation, remapping it from 0..1 to something like 1500..3000K gives a usable temperature but the exact meaning is unclear.

Smoke simulation produces an "heat" attribute. "flame" is here to distinguish flames from smoke. It is a kind of mask to separate two objects that don't share same properties (shapes, emission, color).

Smoke simulation produces an "heat" attribute. "flame" is here to distinguish flames from smoke. It is a kind of mask to separate two objects that don't share same properties (shapes, emission, color).

What we are after is temperature, which is not the same thing as heat technically, but let's ignore the naming for now. The mapping in the code is:

// default values in UI
ignition_temperature = 1.25
maximum_temperature = 1.75
// in smoke simulation code
heat = (1 - flame) * ignition_temperature + flame * maximum_temperature
// when reading grid from Cycles (presumably to bring roughly into 0..1 range)
heat *= 0.5

That multiplication by 0.5 we can get rid of. So then we are left with a temperature, with unit 1/1000 K I guess? Changing the ignition and max temperature affects the smoke simulation, so to make all the physics consistent we could use the heat grid and multiply by 1000 in the shader by default. That looks a bit underwhelming with the default fire sim settings though, more like a little flame than a fire or explosion.

For OpenVDB I think it's more common to store a "temperature" grid with values in range 0..1, where 1 corresponds to some high temperature like 5000K. For easier interop with other applications we could do something like that as well, though the choice of high temperature is pretty arbitrary.

Heat can help to color simulations containing an emitter with a temperature difference positive and an emitter with a temperature difference negative.
Are these kind of smoke simulations without fire producing a null flame supported ?

While I appreciate that this node will load grids automatically I would also like to have a seperate node to load density grids because the current way of using the attribute input is kinda sub-par. Usually I end up doing further operations on the density before feeding it into the volume nodes.
Btw: There are many types of grids to support, for example image sequences/slices like the ones you get from CT or MRT scans. Loading those is possible in Blender internal and it is really useful.

Heat can help to color simulations containing an emitter with a temperature difference positive and an emitter with a temperature difference negative.
Are these kind of smoke simulations without fire producing a null flame supported ?

You can do whatever you want with the heat grid values, but I don't think the Principled Volume node should do this. It seems that the heat grid has totally different values depending if there's fire anywhere in the domain, and that smoke high resolution does not give it extra detail like density or flame. So I think I'll just remap the "flame" grid to a reasonable "temperature" grid in Kelvin useful for fire, and users can do manual setups if they want something else.

  • Add new "temperature" attribute, created from "flame" with range 0..1 mapping to 0..1000K
  • Increase smoke simulation default temperatures to give better looking fire.
  • Emission strength is now added in addition to blackbody, instead of multiplied.
  • Unpremultiply density from "color" attribute automatically, also with Attribute node.
  • Add Density/Color/Temperature Attribute string sockets.
  • Leave Color Attribute empty by default, to avoid the performance impact.
  • Tweak look of RGBA and string sockets to put text on the right, looks better IMO.

This is what the node looks like now:

I'm not entirely happy with it yet, but it's closer.

I'm not entirely happy with it yet, but it's closer.

No guarantee for instant happiness, yet maybe worth dropping a link: I've been having this discussion about controlling absorption / scattering coefficients with WYSIWYG colors just a few days ago.

I'm not entirely happy with it yet, but it's closer.

I've been having this discussion about controlling absorption / scattering coefficients with WYSIWYG colors just a few days ago.

Having a "color at depth" option like in Luxrender would indeed be helpful when doing visualization work. I actually created a node group for that back in the time when there was no volume absorption yet - maybe I can find it again...

Having a "color at depth" option like in Luxrender would indeed be helpful when doing visualization work.

Well, my point was that it might be cool having visible control colors. Setting 'density' or 'reference distance' (its reciprocal) is kind-of a "necessary evil" to pinpoint the transmission color to a fixed frame of reference.

Control colors vs. render:

Applied to volume texturing in a Cycles OSL shader:

Well, my point was that it might be cool having visible control colors. Setting 'density' or 'reference distance' (its reciprocal) is kind-of a "necessary evil" to pinpoint the transmission color to a fixed frame of reference.

Hmm, I don't see a difference to our current handling of colors for volumes. Right now we have color fields to set the color in a WYSIWYG fashion (check the volume absorption node in Cycles for example) but we only have an abstract "density" slider for the strength of the effect. In the screenshots you showed it looks like there is a WYSIWYG color field like in Blender but with a distance setting below it (instead of density) which seems to be the same thing I would like to have?

Right now we have color fields to set the color in a WYSIWYG fashion (check the volume absorption node in Cycles for example)

Only as long as there's only absorption, in which case the calculation degenerates and the absorption coefficient trivially equals the transmission coefficient. Once you add scattering, the whole thing becomes rather unintuitive.
The commits here (among other things) seem to attempt an automatic calculation of the scattering coefficient (and have been jumping back and forth on that), this is why I posted the link.

Emission and blackbody are most likely to change still. The way volume attribute names are exposed I'm also not entirely sure about, but it's pretty useful to have them as sockets so that they can also be exposed in group nodes. Eventually we want almost all node options to be available as sockets anyway for that reason.

I'm actually pretty satisfied with the interpretation of albedo and absorption color in the principled volume shader (and I haven't made any changes to it so far).

  • Density and reference distance (1/density) are two ways to specify the same thing. For the principled volume shader I prefer density, since this naturally corresponds to the density field that you get from volume data. Density is also better to texture, as reference distance values go to zero the density goes to infinity which is not usually what you want. I think reference distance would be something to consider when we add absorption to the glass or principled BSDF. Its advantage is that you can specify a distance which may be more intuitive than a density, but it's less meaningful when multiplied with a volume density grid.
  • The suggested -log(absorption_color) instead of 1 - absorption_color is worth testing, it will then give the exact color at one specific distance. In practice this color various anyway because there is no fixed distance, never mind the interaction with scattering, so it's always a fairly approximate thing anyway.
  • The specific way albedo and absorption color are combined in principled volume shader has a reason behind it. It makes the absorption color affect the color of shadowed areas, while leaving the overall color of the volume mostly controlled by the main color parameter.

This is all mainly for volumes with dominant single scattering. For dense volumes with multiple scattering you need a very different parametrization, and for that we have SSS.

  • Density and reference distance (1/density) are two ways to specify the same thing. For the principled volume shader I prefer density, since this naturally corresponds to the density field that you get from volume data. Density is also better to texture, as reference distance values go to zero the density goes to infinity which is not usually what you want. I think reference distance would be something to consider when we add absorption to the glass or principled BSDF. Its advantage is that you can specify a distance which may be more intuitive than a density, but it's less meaningful when multiplied with a volume density grid.

Luxrender actually lets the user select whether he wants density or distance. Could be handled in the node via a tab.

Having a "color at depth" option like in Luxrender would indeed be helpful when doing visualization work.
[...] Could be handled in the node via a tab.

...or in an external utility node?

  • The suggested -log(absorption_color) instead of 1 - absorption_color is worth testing, it will then give the exact color at one specific distance. In practice this color various anyway because there is no fixed distance, never mind the interaction with scattering, so it's always a fairly approximate thing anyway.

Leaving approximative grounds and accounting for the interaction with scattering was basically what I was suggesting. It's not that hard, gives nice control, and boils down to one color multiplication and subtraction for the implementation:

  • The specific way albedo and absorption color are combined in principled volume shader has a reason behind it. It makes the absorption color affect the color of shadowed areas, while leaving the overall color of the volume mostly controlled by the main color parameter.

You might still be able to achieve that goal when going for albedo & transmission by more physical terms:

albedo = sigma_s / sigma_t  // o-o color
sigma_t = sigma_s + sigma_a // o-o 1 - alpha

This is all mainly for volumes with dominant single scattering. For dense volumes with multiple scattering you need a very different parametrization, and for that we have SSS.

Solving those for sigma_a and sigma_s gets me the colors onto the screen in (almost) any case. Of course you'd have to try whether it still works well for the VFX use cases you are addressing, but since albedo does not depend on density, I guess it might work just fine, stay more intuitive when you crank up colors or density, and enable color texturing (and depending on how that is wired up then, we'd have the ethereal or dense case or both / something in between).

Independent of that, since SSS is limited to (diffuse) translucency and volume shading is the way to go for any transparent materials with visible internal structure, "Principled Density Grid Shader" or "Principled Volume Effects Shader" could be more appropriate names for this one in case it can't (or shouldn't) be generalized.

I did some more testing with the scattering and absorption colors and adjusted the code. Here's the new behavior, with and without volume bounces. The colored planes in front show the scatter/albedo and absorption/transmission color parameters.

Here's the albedo + transmission parameters as suggested by @Tobias Schwinger (tschw).

The thing I think is problematic with -log(transmission_color) is that it affects the density of the volume. Black makes the volume very dense, white makes it fully transparent, with the neutral value being 1/e = 0.3678. Adjusting the density is exactly what is required to get a specific color at a specified depth, but that seems more appropriate for a glass absorption like shader.

I think the name Principled Volume is ok, it doesn't explain the whole thing but for most users adding more technical terms to the name is not so helpful. It's better explained with text and renders in the docs. Other renderers also typically called it just "Something Volume".

  • New scatter and absorption color parameter interpretation.
  • Blackbody Tint parameter to decouple it from Emission parameters entirely.
  • Rename Emission to Emission Strength.

This is ready for code review and committing now I think.

I did some more testing with the scattering and absorption colors and adjusted the code. Here's the new behavior, with and without volume bounces. The colored planes in front show the scatter/albedo and absorption/transmission color parameters.

Lookin' good! Glad I could inspire you a little :-).

color absorption_coeff = max(1.0 - scatter_color, 0.0) * max(1.0 - AbsorptionColor, 0.0);
// (1-s)(1-a) = 1-s-a+sa

AbsorptionColor actually now describes transmission (at some nonlinear scale, due to that sa term that keeps it from underflowing with the chosen inputs).

Is it much better than before? If not, I'd go without, because it's easier to reparametrize externally with just coeff = color * density.

The thing I think is problematic with -log(transmission_color) is that it affects the density of the volume. Black makes the volume very dense, white makes it fully transparent, with the neutral value being 1/e = 0.3678. Adjusting the density is exactly what is required to get a specific color at a specified depth, but that seems more appropriate for a glass absorption like shader.

That's what I figured, hence the suggestion to use an extra node for that stuff.

color absorption_coeff = max(1.0 - scatter_color, 0.0) * max(1.0 - AbsorptionColor, 0.0);
// (1-s)(1-a) = 1-s-a+sa

AbsorptionColor actually now describes transmission (at some nonlinear scale, due to that sa term that keeps it from underflowing with the chosen inputs).
Is it much better than before? If not, I'd go without, because it's easier to reparametrize externally with just coeff = color * density.

We have low level volume scatter and absorption shader nodes for those who want to do a custom parametrization.

But yes I think it is better than before. AbsorptionColor is not really transmission, because it does not affect the scattering coefficient. If it did, the volume density would change with the absorption color, which is what we want to avoid. The purpose of the absorption color here is to tint the shadow colors after you've chosen a scattering color.

color absorption_coeff = max(1.0 - scatter_color, 0.0) * max(1.0 - AbsorptionColor, 0.0);
// (1-s)(1-a) = 1-s-a+sa

AbsorptionColor actually now describes transmission (at some nonlinear scale, due to that sa term that keeps it from underflowing with the chosen inputs).
Is it much better than before? If not, I'd go without, because it's easier to reparametrize externally with just coeff = color * density.

We have low level volume scatter and absorption shader nodes for those who want to do a custom parametrization.

For those, boils down to many nodes to brutally evaluate :-/.

But yes I think it is better than before. AbsorptionColor is not really transmission, because it does not affect the scattering coefficient. If it did, the volume density would change with the absorption color, which is what we want to avoid. The purpose of the absorption color here is to tint the shadow colors after you've chosen a scattering color.

Well, here it becomes somewhat philosophical from which angle you want to look at it, I guess: Above code (and the expansion) is kind of a subtraction (one that can't underflow) and the renderer adds scattering and absorption coefficients together to yield transmission. Because of that, it happens to work for you color-wise. The way I look at it, it's transmission on a scale biased by scattering (keeping that one as-is). Could also say it's absorption, where the effect of (as-is) out-scattering gets cancelled out. Both are just two different ways of telling the same story. It really is neither transmission nor absorption, but something in-between (based on what the given scattering value is) :-).

This revision was not accepted when it landed; it landed in state Needs Review.Feb 23 2018, 7:02 PM
This revision was automatically updated to reflect the committed changes.