Share

Volume Shaders - Arnold Developer Guide

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 1m.

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 shader 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:

simple_volume.cpp

#include "ai.h"
#include <string.h>

AI_SHADER_NODE_EXPORT_METHODS(SimpleVolumeMethods);


enum SimpleVolumeParams
{
   p_position,
   p_radius,
   p_octaves,
   p_scale
};

node_parameters
{
   AiParameterVec ("position", 0.0f, 0.0f, 0.0f);
   AiParameterFlt ("radius"  , 1.0f);
   AiParameterInt ("octaves" , 1);
   AiParameterFlt ("scale"   , 2.0f);
}

node_initialize
{
}

node_update
{
}

node_finish
{
}

shader_evaluate
{
   AtVector c = AiShaderEvalParamVec(p_position);
   float r = AiShaderEvalParamFlt(p_radius);
   int octaves = AiShaderEvalParamInt(p_octaves);
   float scale = AiShaderEvalParamFlt(p_scale);

   float density = 50.f;

   AtVector p = sg->Po;
   float rel_dist = AiV3Length(p - c) / r;                         // in [ 0,1]
   float noise = AiNoise3(p * scale, octaves, 0, 1.92f);           // in [-1,1]
   float threshold = rel_dist * 2 - 1;                             // in [-1,1]
   noise = noise > threshold ? density : 0;
   AtRGB noise_RGB(noise);

   sg->out.CLOSURE() = AiClosureVolumeHenyeyGreenstein(sg, AI_RGB_BLACK, noise_RGB, AI_RGB_BLACK);
}

node_loader
{
   if (i>0)
      return false;
   node->methods     = (AtNodeMethods*) SimpleVolumeMethods;
   node->output_type = AI_TYPE_CLOSURE;
   node->name        = "simple_volume";
   node->node_type   = AI_NODE_SHADER;
   strcpy(node->version, AI_VERSION);
   return true;
}

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)

example_scene.ass

options
{
 AA_samples 9
 outputs "RGBA RGBA /out/arnold1:gaussian_filter /out/arnold1:jpeg"
 xres 640
 yres 480
 GI_diffuse_depth 1
 GI_specular_depth 1
 GI_diffuse_samples 3
}

driver_jpeg
{
 name /out/arnold1:jpeg
 filename "simple.jpg"
}

gaussian_filter
{
 name /out/arnold1:gaussian_filter
}

persp_camera
{
  name mycamera
  position 0 0.17 0.8
  look_at 0 -0.05 0
  up 0 1 0
  fov 36
  handedness left
}


skydome_light
{
 name mysky
 intensity 0.1
 camera 0
}

lambert 
{
  name mylambert
  Kd_color  1 0.5 0.1
  Kd 0.93
}

quad_light
{
  name mylight
  vertices 4 1 POINT -0.0125 0.20 0.0125   -0.0125 0.2 0.0  0.0125 0.2 0.0   0.0125 0.2 0.0125
  color  0.7931 0.7931 1
  intensity 1600
  normalize off
  samples 1
  volume_samples 2
}

simple_volume
{
 name myvolume
 radius 1.0
 octaves 3
 scale 5
}

box
{
 name myBox
 min  -1 -1 -1
 max   1  1  1
 step_size 0.05
 shader myvolume
 matrix
  0.1 0 0 0
  0 0.1  0 0
  0 0 0.1 0
  -0.1 -0.1 0 1
}

box
{
 name myBox
 min  -1 -1 -1
 max   1  1  1
 step_size 0.05
 shader myvolume
 matrix
  0.12 0 0 0
  0 0.2 0 0
  0 0 0.12 0
  0.1 -0.05 0 1
}


plane
{
 name myplane
 point 0 -0.2 0
 normal 0 1 0
 shader mylambert
}

volume shader

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.

Was this information helpful?