Volume rendering transfer function

I’m trying to set up a scientific visualization of a volumetric dataset in VDB format that has a single field value, 32-bit floating point in the range [0,1].


Usually in sci-viz volume rendering you specify a transfer function that maps a value for a certain location (i.e. shading point) within the volume into a color and an opacity value. Sometimes such a transfer function combines the two, outputting both color and opacity, but in other cases you can also specify them separately, one function for color, one function for opacity.

I see the Principled Volume node (in 2.92) has inputs for color and density, but I’m having a hard time setting up some kind of “transfer function” using (say) the ColorRamp node and the available input nodes. So I’m wondering whether the current volume object and rendering setup supports such general sci-viz style volume rendering, given that it seems to be aimed mostly at rendering clouds, smoke, fire, etc.


  • The Attribute node, when set to type Geometry, has a tooltip that says " within the volume", which sounds like it can provide volume-based information for a shading point. But setting the Name value to the volume attribute doesn’t seem to have any effect on the opacity during rendering in this node setup:

  • Judging by the docs the Volume Info node seems to be specific to smoke domains, but does that mean it won’t work on general VDB volumes? And does it provide values for the complete domain, or is it like Geometry in that gives values per shading point? I assume the latter.

  • The Geometry node might be another candidate to use for defining a transfer function, as its Position output might be used to sample the volume (which could then be passed through a ColorRamp, etc), but I don’t see any volume sampler node.

  • How does the Density input versus the Density Attribute behave (and similarly for Color)? The docs don’t really say much on the relation between the two. Does one override the other when set? Doing some tests with the possible combinations of connecting/setting suggests there is something more complex going on.

  • Is the density value used in Blender directly mapped to opacity during rendering?

Thanks in advance for any feedback

The Attribute node reads the value from the specified volume grid at the current shading position. Not sure why this specific setup is not working, hard to tell from the screenshot alone. Does the volume show up correctly in the basic viewport? Is there any lighting to illuminate the volume, or a background to contrast it against?

Volume Info is just a convenience node that is equivalent to Attribute nodes with Name set to density, temperature, etc.

The Density and Density Attribute are multiplied together, if a density attribute is specified.

For the color you can check the implementation:

The relation between density and opacity is as follows:

opacity = exp(-density * distance)

Volume shading nodes do not have a notion of volume segment distance, they are evaluated at multiple points by the integrator, and integration over distance happens after the shading node execution. So formulating the volume shader in terms of transfer functions is not possible.

Hah, I seem to have been unlucky in the combinations I tried before posting. Setting the density attribute to value was the only thing I needed to change to make it work:

Hmm, this is an OSL shader, yet I don’t have OSL enabled? But I guess the non-OSL implementation is similar?

By the way, just got a segfault after enabling OSL for CPU rendering (supported feature set) on the above scene:

$ cat /tmp/mummy.crash.txt
# Blender 2.92.0, Commit date: 2021-02-24 16:25, Hash 02948a2cab44
bpy.context.scene.cycles.device = 'CPU'  # Property
bpy.context.scene.cycles.shading_system = True  # Property

# backtrace
blender(BLI_system_backtrace+0x34) [0x55b02c06c084]
blender(+0xe151bd) [0x55b029b431bd]
/usr/lib/libc.so.6(+0x3cf80) [0x7fef61999f80]
/usr/lib/liboslexec.so.1.11(_ZN9OSL_v1_113pvt9LLVM_Util13call_functionEPN4llvm5ValueEN16OpenImageIO_v2_24spanIKS4_Lln1EEE+0x160) [0x7fef685190d0]
/usr/lib/liboslexec.so.1.11(_ZN9OSL_v1_113pvt16llvm_gen_closureERNS0_11BackendLLVMEi+0x2f5) [0x7fef684f1215]
/usr/lib/liboslexec.so.1.11(_ZN9OSL_v1_113pvt11BackendLLVM15build_llvm_codeEiiPN4llvm10BasicBlockE+0x196) [0x7fef685097b6]
/usr/lib/liboslexec.so.1.11(_ZN9OSL_v1_113pvt11BackendLLVM19build_llvm_instanceEb+0xab6) [0x7fef6850cc36]
/usr/lib/liboslexec.so.1.11(_ZN9OSL_v1_113pvt11BackendLLVM3runEv+0x6ba) [0x7fef6850e70a]
/usr/lib/liboslexec.so.1.11(_ZN9OSL_v1_113pvt17ShadingSystemImpl14optimize_groupERNS_11ShaderGroupEPNS_14ShadingContextEb+0x4cd) [0x7fef683b790d]
/usr/lib/liboslexec.so.1.11(_ZN9OSL_v1_113pvt17ShadingSystemImpl19optimize_all_groupsEiiib+0x6ab) [0x7fef683ba89b]
/usr/lib/libstdc++.so.6(+0xcfbc4) [0x7fef61d3ebc4]
/usr/lib/libpthread.so.0(+0x9299) [0x7fef6b238299]
/usr/lib/libc.so.6(clone+0x43) [0x7fef61a5c053]

(gdb) bt
#0  0x0000000040f5b083 in ?? ()
#1  0x00007fef280348d0 in ?? ()
#2  0x00007fef00000000 in ?? ()
#3  0x0000000400000001 in ?? ()
#4  0x0000000000000780 in ?? ()
#5  0x00007ffd91014ad0 in ?? ()
#6  0x00007fef341be62d in ?? () from /usr/lib/libnvidia-glcore.so.460.56
#7  0x0000000000000780 in ?? ()
#8  0x0000000100000000 in ?? ()
#9  0x00007ffd91014ad0 in ?? ()
#10 0x00007ffd91014ad0 in ?? ()
#11 0x00000000413fc000 in ?? ()
#12 0x00007fef356c6ca0 in ?? () from /usr/lib/libnvidia-glcore.so.460.56
#13 0x00007fef341be5d0 in ?? () from /usr/lib/libnvidia-glcore.so.460.56
#14 0x00007fef2d632900 in ?? ()
#15 0x0000000000000000 in ?? ()

Is this worthy of a bug report?

Edit: this seems to be unrelated to volume rendering. Just enabling OSL shading on my system causes a crash. I’ll looking into it a bit more.

Thanks for the other info!

Also, curious thing I don’t understand. If I volume import the disney cloud, specifically wdas_cloud_quarter.vdb, it gets added to the scene with a +90 rotation in X. While if I add my own vdb, generated through pyopenvdb, it does not get that rotation. The transformations in the metadata do not contain any rotation, only a scale. I can’t find definitions for most of the metadata fields, but do they cause the difference?

melis@juggle 20:29:~/courses/blender-course/webcontent/modules/advanced/volumes$ vdb_print -m /extra/data/wdas_cloud/wdas_cloud_quarter.vdb 
creator: Houdini/SOP_OpenVDB_Write
density      float (-261,-81,-358)->(236,256,254) 498x338x613  24.1MVox 9.41MB
   background: 0
   voxel size: 0.833
   index to world:
      [0.833, 0, 0, 0] 
      [0, 0.833, 0, 0] 
      [0, 0, 0.833, 0] 
      [0, 0, 0, 1] 
   class: fog volume
   creator: Houdini 16.0.504.20/GEO_VDBTranslator
   file_bbox_max: [236, 256, 254]
   file_bbox_min: [-261, -81, -358]
   file_compression: active values
   file_mem_bytes: 92494808
   file_voxel_count: 24063202
   is_local_space: false
   is_saved_as_half_float: false
   name: density
   value_type: float
   vector_type: invariant
   voxelsize: 0
melis@juggle 20:32:~/courses/blender-course/webcontent/modules/advanced/volumes$ vdb_print -m mummy.vdb 
value        float (0,0,3)->(127,127,127)       128x128x125   1.6MVox  779KB
   background: 0
   voxel size: 1
   index to world:
      [1, 0, 0, 0] 
      [0, 1, 0, 0] 
      [0, 0, 1, 0] 
      [0, 0, 0, 1] 
   file_bbox_max: [127, 127, 127]
   file_bbox_min: [0, 0, 3]
   file_compression: blosc + active values
   file_mem_bytes: 7633632
   file_voxel_count: 1600201
   name: value

Edit: ah, wasn’t expecting such a dirty trick :slight_smile:

bool BKE_volume_is_y_up(const Volume *volume)
  /* Simple heuristic for common files to open the right way up. */
  VolumeGridVector &grids = *volume->runtime.grids;
  if (grids.metadata) {
    openvdb::StringMetadata::ConstPtr creator =
    if (!creator) {
      creator = grids.metadata->getMetadata<openvdb::StringMetadata>("Creator");
    return (creator && creator->str().rfind("Houdini", 0) == 0);

  return false;