Volume Shading API

Volume shaders output closures describing absorption, scattering and emission. These are the available closures:

Volume Closures
AI_API AtClosure AiClosureVolumeAbsorption(const AtShaderGlobals* sg, const AtRGB& weight);
AI_API AtClosure AiClosureVolumeEmission(const AtShaderGlobals* sg, const AtRGB& weight);
AI_API AtClosure AiClosureVolumeHenyeyGreenstein(const AtShaderGlobals* sg,
   const AtRGB& absorption, const AtRGB& scattering, const AtRGB& emission, float g = 0.f);
AI_API AtClosure AiClosureVolumeMatte(const AtShaderGlobals* sg, const AtRGB& weight);

You can think of volumes as a sort of point cloud with infinitesimally small motes. When rays of light traverse a volume they may either hit a mote and be reflected/scattered (think white motes), they may hit a mote and be absorbed (think black motes), or they may traverse the volume without hitting anything at all. Depending on how tightly packed together the motes are, there will be a greater or lesser chance of actually hitting a mote, and this chance increases the greater the distance that the ray traverses in the volume. The weights of the volume closures are the absorption, scattering and emission coefficients, which are rates with unit \( \dfrac{1}{m}. \)


The emission coefficient is the rate at which a volume emits light at a given point. A ray traversing a volume with a constant emission coefficient will have radiance added at a rate of emission_coefficient * distance_traveled. The light emitted by a volume is visible to global illumination. It will also be affected by any attenuation, out-scattering or absorption effects in the volume.

The scattering coefficient is the rate at which light is scattered (or reflected) at a given point. The greater the rate of scattering, the shorter the average distance a ray of light will travel through a volume before being bounced off of its course. There is an optional parameter for the Henyey-Greenstrein closure to describe the mean cosine of the direction of the scattered ray with respect to that of the original ray (g). Valid values for g are anywhere between -1 (full back-scatter) and 1 (full forward-scatter), and by default, the mean cosine is set to 0 (isotropic scattering).

The absorption coefficient is the rate at which light is absorbed at a given point. Summing the absorption and scattering coefficients gives the attenuation coefficient (also called the extinction coefficient), which corresponds to the overall density of the volume.


Volume shaders often do not expose absorption and scattering coefficients as parameters directly. Instead, it is more intuitive to specify a density (attenuation coefficient) and scattering color (albedo). As long as the scattering color is in the range 0..1, the volume shader will be energy conserving. Values outside the range may be used for non-physically real scattering effects.

Volume Density and Scattering Color
shader_evaluate
{
   const float density = AiShaderEvalParamFlt(p_density);
   const AtRGB scattering_color = AiShaderEvalParamRGB(p_scattering_color);
   const float anisotropy = AiShaderEvalParamFlt(p_anisotropy);
 
   const AtRGB absorption = density * (1 - scattering_color);
   const AtRGB scattering = density * scattering_color;
   const AtRGB emission = AI_RGB_BLACK;
 
   sg->out.CLOSURE() = AiClosureVolumeHenyeyGreenstein(sg,
      absorption, scattering, emission, anisotropy);
}

Shader Globals

Volumetric shaders can expect the following shader globals to be readily available for use:

  • sg->Rd : direction of ray traversing volume
  • sg->Ro : position indicating the beginning of the segment of the volume that is being evaluated
  • sg->Rl : length of the segment of the volume that is being evaluated
  • sg->P : world-space sample position within the volume segment that is being evaluated
  • sg->Po : object-space sample position within the volume segment that is being evaluated
  • sg->M : object to world space transform matrix
  • sg->Minv: world to object space transform matrix
  • sg->Op : pointer to volume's container shape
  • sg->dPdx: rate of change of sg->P as samples move along the x-axis of the image plane
  • sg->dPdy: rate of change of sg->P as samples move along the y-axis of the image plane

Here is a diagram of these shader globals:


Example Shader

Here is a small example of a volume shade that places a heterogeneous volume, modulated by a procedural noise effect and a spherical bounds, in the object space of the volume's container shape:

Example Volume Shader
shader_evaluate
{
   const AtVector c = AiShaderEvalParamVec(p_position);
   const float r = AiShaderEvalParamFlt(p_radius);
   const int octaves = AiShaderEvalParamInt(p_octaves);
   const float scale = AiShaderEvalParamFlt(p_scale);
 
   const float density = 25.f;
 
   const AtVector p = sg->Ro;
   const float rel_dist = AiV3Length(p - c) / r;         // in [ 0,1]
   const float threshold = rel_dist * 2 - 1;             // in [-1,1]
   float noise = AiNoise3(p * scale, octaves, 0, 1.92f); // in [-1,1]
   noise = noise > threshold ? density : 0;

   const AtRGB absorption = AI_RGB_BLACK;
   const AtRGB scattering = AtRGB(noise);
   const AtRGB emission = AI_RGB_BLACK;

   sg->out.CLOSURE() = AiClosureVolumeHenyeyGreenstein(sg,
      absorption, scattering, emission);
}


When applied to two different container shapes, the left shape with a vertical scaling that is twice that of the right shape, the results from this shader could look like this: (remember to set step_size on the bounding shapes, and options.GI_volume_samples if you want indirect lighting on the volume)


Note that, because volume shaders can be called dozens or even hundreds of times per ray, the shader_evaluate code must be as efficient as possible. Even harmless looking calls like AiShaderEvalParam* can add significant overhead if used excessively so you will want to keep those to a minimum or precompute them in node_update when possible.


  • No labels