Visualization Library v1.0.3

A lightweight C++ OpenGL middleware for 2D/3D graphics

VL     Star     Watch     Fork     Issue

[Download] [Tutorials] [All Classes] [Grouped Classes]

Sliced Volume Rendering with Transfer Functions and Lighting Tutorial

This tutorial demonstrates how to use the vl::SlicedVolume class to render a volume in various ways.

pagGuideSlicedVolume_1.jpg
pagGuideSlicedVolume_2.jpg
pagGuideSlicedVolume_4.jpg
pagGuideSlicedVolume_5.jpg
pagGuideSlicedVolume_6.jpg
pagGuideSlicedVolume_7.jpg

The example below demonstrates how to use an arbitrary GLSL program and transfer function to render a volume. The fragment shader used in this example is capable of computing the volume gradient and per-pixel lighting in real-time with up to 4 lights at the same time. It's also capable of taking advantage of a precomputed normal-texture to speedup the lighting computations. You can drag and drop in the window any supported volume data to visualize it at different threshold level. The threshold level can be modified using the mouse wheel.

[From App_VolumeSliced.cpp]

#include "BaseDemo.hpp"
#include <vlVolume/SlicedVolume.hpp>
#include <vlVolume/VolumeUtils.hpp>
#include <vlGraphics/Text.hpp>
#include <vlGraphics/FontManager.hpp>
#include <vlGraphics/GLSL.hpp>
#include <vlGraphics/GeometryPrimitives.hpp>

using namespace vl;

/* ----- sliced volume visualization settings ----- */

/* Use OpenGL Shading Language to render the volume. */
static bool USE_GLSL = true;

/* If enabled, renders the volume using 3 animated lights. Requires USE_GLSL. */
static bool DYNAMIC_LIGHTS = true; 

/* If enabled, a white transfer function is used and 3 colored lights 
   are used to render the volume. Used only if DYNAMIC_LIGHTS is true. */
static bool COLORED_LIGHTS = true;

/* Use a separate 3d texture with a precomputed gradient to speedup the fragment shader.
   Requires more memory (for the gradient texture) but can speedup the rendering. */
static bool PRECOMPUTE_GRADIENT = false; // only if USE_GLSL is true.

/* The number of slices used to render the volume, the higher the number the better 
  (and slower) the rendering will be. */
static const int  SLICE_COUNT = 1000;

/* Our applet used to render and interact with the volume. */
class App_VolumeSliced: public BaseDemo
{
public:

  virtual String appletInfo()
  {
    return BaseDemo::appletInfo() + 
    "- Use the mouse wheel to change the bias used to render the volume.\n" +
    "- Drop inside the window a set of 2D files or a DDS or DAT volume to display it.\n" +
    "\n";
  }

  /* initialize the applet with a default volume */
  virtual void initEvent()
  {
    Log::notify(appletInfo());
    
    if (!Has_Texture_3D && !Has_GLSL)
    {
      Log::error("This test requires 3D texture or GLSL support!\n");
      vl::Time::sleep(2000);
      exit(1);
    }

    // variable preconditions
    USE_GLSL &= Has_GLSL;
    DYNAMIC_LIGHTS &= USE_GLSL;
    COLORED_LIGHTS &= DYNAMIC_LIGHTS;
    PRECOMPUTE_GRADIENT &= USE_GLSL;

    // lights to be used later
    mLight0 = new Light;
    mLight1 = new Light;
    mLight2 = new Light;

    // you can color the lights!
    if (DYNAMIC_LIGHTS && COLORED_LIGHTS)
    {
      mLight0->setAmbient(fvec4(0.1f, 0.1f, 0.1f, 1.0f));
      mLight1->setAmbient(fvec4(0.0f, 0.0f, 0.0f, 1.0f));
      mLight2->setAmbient(fvec4(0.0f, 0.0f, 0.0f, 1.0f));
      mLight0->setDiffuse(vl::gold);
      mLight1->setDiffuse(vl::green);
      mLight2->setDiffuse(vl::royalblue);
    }

    // light bulbs
    if (DYNAMIC_LIGHTS)
    {
      mLight0Tr = new Transform;
      mLight1Tr = new Transform;
      mLight2Tr = new Transform;
      rendering()->as<Rendering>()->transform()->addChild( mLight0Tr.get() );
      rendering()->as<Rendering>()->transform()->addChild( mLight1Tr.get() );
      rendering()->as<Rendering>()->transform()->addChild( mLight2Tr.get() );
      mLight0->bindTransform( mLight0Tr.get() );
      mLight1->bindTransform( mLight1Tr.get() );
      mLight2->bindTransform( mLight2Tr.get() );

      ref<Effect> fx_bulb = new Effect;
      fx_bulb->shader()->enable(EN_DEPTH_TEST);
      ref<Geometry> light_bulb = vl::makeIcosphere(vec3(0,0,0),1,1);
      sceneManager()->tree()->addActor( light_bulb.get(), fx_bulb.get(), mLight0Tr.get() );
      sceneManager()->tree()->addActor( light_bulb.get(), fx_bulb.get(), mLight1Tr.get() );
      sceneManager()->tree()->addActor( light_bulb.get(), fx_bulb.get(), mLight2Tr.get() );
    }

    ref<Effect> vol_fx = new Effect;
    vol_fx->shader()->enable(EN_DEPTH_TEST);
    vol_fx->shader()->enable(EN_BLEND);
    vol_fx->shader()->setRenderState( mLight0.get(), 0 );
    // add the other lights only if dynamic lights have to be displayed
    if (DYNAMIC_LIGHTS)
    {
      vol_fx->shader()->setRenderState( mLight1.get(), 1 );
      vol_fx->shader()->setRenderState( mLight2.get(), 2 );
    }

    // The GLSL program used to perform the actual rendering.
    // The \a volume_luminance_light.fs fragment shader allows you to specify how many 
    // lights to use (up to 4) and can optionally take advantage of a precomputed 
    // normals texture.
    if (USE_GLSL)
    {
      mGLSL = vol_fx->shader()->gocGLSLProgram();
      mGLSL->attachShader( new GLSLFragmentShader("/glsl/volume_luminance_light.fs") );
      mGLSL->attachShader( new GLSLVertexShader("/glsl/volume_luminance_light.vs") );
    }

    // transform and trackball setup
    mVolumeTr = new Transform;
    trackball()->setTransform( mVolumeTr.get() );

    // volume actor
    mVolumeAct = new Actor;
    mVolumeAct->setEffect( vol_fx.get() );
    mVolumeAct->setTransform(mVolumeTr.get());
    sceneManager()->tree()->addActor( mVolumeAct.get() );

    // sliced volume: will generate the actual actor's geometry on the fly.
    mSlicedVolume = new vl::SlicedVolume;
    mSlicedVolume->bindActor(mVolumeAct.get());
    mSlicedVolume->setSliceCount(SLICE_COUNT);
    AABB volume_box( vec3(-10,-10,-10), vec3(+10,+10,+10) );
    mSlicedVolume->setBox(volume_box);

    // volume bounding box outline
    ref<Effect> fx_box = new Effect;
    fx_box->shader()->gocPolygonMode()->set(PM_LINE, PM_LINE);
    fx_box->shader()->enable(EN_DEPTH_TEST);
    fx_box->shader()->gocColor()->setValue(vl::red);
    ref<Geometry> box_outline = vl::makeBox(volume_box);
    sceneManager()->tree()->addActor( box_outline.get(), fx_box.get(), mVolumeTr.get() );

    // bias text
    mBiasText = new Text;
    mBiasText->setFont( defFontManager()->acquireFont("/font/bitstream-vera/VeraMono.ttf", 12) );
    mBiasText->setAlignment( AlignHCenter | AlignBottom);
    mBiasText->setViewportAlignment( AlignHCenter | AlignBottom );
    mBiasText->translate(0,5,0);
    mBiasText->setBackgroundEnabled(true);
    mBiasText->setBackgroundColor(fvec4(0,0,0,0.75));
    mBiasText->setColor(vl::white);
    ref<Effect> effect = new Effect;
    effect->shader()->enable(EN_BLEND);
    sceneManager()->tree()->addActor(mBiasText.get(), effect.get());

    // bias uniform
    mVolumeAct->gocUniform("val_threshold")->setUniformF(0.5f);
    mAlphaBias = mVolumeAct->getUniform("val_threshold");

    // update alpha bias text
    mouseWheelEvent(0);

    // let's get started with the default volume!
    setupVolume( loadImage("/volume/VLTest.dat") );
  }

  /* load files drag&dropped in the window */
  void fileDroppedEvent(const std::vector<String>& files)
  {
    if(files.size() == 1) // if there is one file load it directly
    {      
      if (files[0].endsWith(".dat"))
      {
        ref<Image> vol_img = loadImage(files[0]);
        if (vol_img)
          setupVolume(vol_img);
      }
    }
    else // if there is more than one file load all the files and assemble a 3D image
    {      
      // sort files by their name
      std::vector<String> files_sorted = files;
      std::sort(files_sorted.begin(), files_sorted.end());
      // load the files
      std::vector< ref<Image> > images;
      for(unsigned int i=0; i<files_sorted.size(); ++i)
      {
        images.push_back( loadImage(files_sorted[i]) );
        if (files_sorted[i].endsWith(".dcm"))
          images.back()->contrastHounsfieldAuto();
      }
      // assemble the volume
      ref<Image> vol_img = assemble3DImage(images);
      // set the volume
      if (vol_img)
        setupVolume(vol_img);
    }
  }

  /* visualize the given volume */
  void setupVolume(ref<Image> img)
  {
    Effect* vol_fx = mVolumeAct->effect();

    // remove shader uniforms
    vol_fx->shader()->setUniformSet(NULL);
    // remove GLSLProgram
    vol_fx->shader()->eraseRenderState(vl::RS_GLSLProgram);
    // keep texture unit #0
    // vol_fx->shader()->eraseRenderState(RS_TextureSampler,0); 
    // remove texture unit #1 and #2
    vol_fx->shader()->eraseRenderState(RS_TextureSampler, 1);
    vol_fx->shader()->eraseRenderState(RS_TextureSampler, 2);

    if(img->format() == IF_LUMINANCE)
    {
      ref<Image> gradient;
      if (PRECOMPUTE_GRADIENT)
      {
        // note that this can take a while...
        gradient = vl::genGradientNormals(img.get());
      }

      if (USE_GLSL)
      {
        Log::notify("IF_LUMINANCE image and GLSL supported: lighting and the transfer function will be computed in realtime.\n");

        ref<Image> trfunc;
        if (COLORED_LIGHTS)
          trfunc = vl::makeColorSpectrum(128, vl::white, vl::white); // let the lights color the volume
        else
          trfunc = vl::makeColorSpectrum(128, vl::blue, vl::royalblue, vl::green, vl::yellow, vl::crimson);
        // installs GLSLProgram
        vol_fx->shader()->setRenderState(mGLSL.get());
        // install volume image
        vol_fx->shader()->gocTextureSampler(0)->setTexture( new vl::Texture( img.get() ) );
        vol_fx->shader()->gocUniform("volume_texunit")->setUniformI(0);
        mSlicedVolume->generateTextureCoordinates( ivec3(img->width(), img->height(), img->depth()) );
        // installs the transfer function as texture #1
        vol_fx->shader()->gocTextureSampler(1)->setTexture( new Texture( trfunc.get() ) );  
        vol_fx->shader()->gocUniform("trfunc_texunit")->setUniformI(1);
        vol_fx->shader()->gocUniform("trfunc_delta")->setUniformF( 0.5f / trfunc->width() );    
        // pre-computed gradient texture
        if (PRECOMPUTE_GRADIENT)
        {
          vol_fx->shader()->gocUniform("precomputed_gradient")->setUniformI(1);
          vol_fx->shader()->gocTextureSampler(2)->setTexture( new Texture( gradient.get(), TF_RGBA8, false, false ) );
          vol_fx->shader()->gocUniform("gradient_texunit")->setUniformI(2);
        }
        else
        {
          vol_fx->shader()->gocUniform("precomputed_gradient")->setUniformI(0);
          // used to compute on the fly the normals based on the volume's gradient
          vol_fx->shader()->gocUniform("gradient_delta")->setUniform(fvec3(0.5f/img->width(), 0.5f/img->height(), 0.5f/img->depth()));
        }

        // no need for alpha testing, we discard fragments inside the fragment shader
        vol_fx->shader()->disable(EN_ALPHA_TEST);
      }
      else // precompute transfer function and illumination
      {
        Log::notify("IF_LUMINANCE image and GLSL not supported: transfer function and lighting will be precomputed.\n");

        // generate simple transfer function
        ref<Image> trfunc = vl::makeColorSpectrum(128, vl::black, vl::blue, vl::green, vl::yellow, vl::red);
        // precompute volume with transfer function and lighting
        ref<Image> volume = vl::genRGBAVolume(img.get(), trfunc.get(), fvec3(1.0f,1.0f,0.0f));
        vol_fx->shader()->gocTextureSampler(0)->setTexture( new vl::Texture( volume.get() ) );
        mSlicedVolume->generateTextureCoordinates( ivec3(volume->width(), volume->height(), volume->depth()) );
        vol_fx->shader()->enable(EN_ALPHA_TEST);
        vol_fx->shader()->gocAlphaFunc()->set(FU_GEQUAL, 0.3f);
      }
    }
    else // if it's a color texture just display it as it is
    {
      Log::notify("Non IF_LUMINANCE image: not using GLSL.\n");
      // install volume texture
      vol_fx->shader()->gocTextureSampler(0)->setTexture( new vl::Texture( img.get() ) );
      mSlicedVolume->generateTextureCoordinates( ivec3(img->width(), img->height(), img->depth()) );
      // setup alpha test
      vol_fx->shader()->enable(EN_ALPHA_TEST);
      vol_fx->shader()->gocAlphaFunc()->set(FU_GEQUAL, 0.3f);
    }
    
    mAlphaBias->setUniformF(0.3f);
    updateText();
    openglContext()->update();
  }

  void updateText()
  {
    float bias = 0.0f;
    mAlphaBias->getUniform(&bias);
    mBiasText->setText(Say("Bias = %n") << bias);
  }

  void mouseWheelEvent(int val)
  {
    float alpha = 0.0f;
    mAlphaBias->getUniform(&alpha);
    alpha += val * 0.002f;
    alpha =  clamp(alpha, 0.0f, 1.0f);
    mAlphaBias->setUniformF(alpha);

    // used for non GLSL mode volumes
    mVolumeAct->effect()->shader()->gocAlphaFunc()->set(FU_GEQUAL, alpha);
    
    updateText();
    openglContext()->update();
  }

  /* animate the lights */
  virtual void updateScene()
  {
    if (DYNAMIC_LIGHTS)
    {
      mat4 mat;
      // light 0 transform.
      mat = mat4::getRotation( Time::currentTime()*43, 0,1,0 ) * mat4::getTranslation(20,20,20);
      mLight0Tr->setLocalMatrix(mat);
      // light 1 transform.
      mat = mat4::getRotation( Time::currentTime()*47, 0,1,0 ) * mat4::getTranslation(-20,0,0);
      mLight1Tr->setLocalMatrix(mat);
      // light 2 transform.
      mat = mat4::getRotation( Time::currentTime()*47, 0,1,0 ) * mat4::getTranslation(+20,0,0);
      mLight2Tr->setLocalMatrix(mat);
    }
  }

  protected:
    ref<Transform> mVolumeTr;
    ref<Transform> mLight0Tr;
    ref<Transform> mLight1Tr;
    ref<Transform> mLight2Tr;
    ref<Uniform> mAlphaBias;
    ref<Text> mBiasText;
    ref<Light> mLight0;
    ref<Light> mLight1;
    ref<Light> mLight2;
    ref<GLSLProgram> mGLSL;
    ref<Actor> mVolumeAct;
    ref<vl::SlicedVolume> mSlicedVolume;
};

// Have fun!

[From volume_luminance_light.vs]

/**************************************************************************************/
/*                                                                                    */
/*  Copyright (c) 2005-2011, Michele Bosi.                                            */
/*  All rights reserved.                                                              */
/*                                                                                    */
/*  This file is part of Visualization Library                                        */
/*  http://visualizationlibrary.org                                                   */
/*                                                                                    */
/*  Released under the OSI approved Simplified BSD License                            */
/*  http://www.opensource.org/licenses/bsd-license.php                                */
/*                                                                                    */
/**************************************************************************************/

// Simply passes the vertex frag_position and texture coordinate to the fragment shader. 
// It also passes the vertex coord in object space to perform per-pixel lighting.

varying vec3 frag_position; // in object space

void main(void)
{
    frag_position = gl_Vertex.xyz;
    gl_TexCoord[0] = gl_MultiTexCoord0;
    gl_Position = ftransform();
}
// Have fun!

[From volume_luminance_light.fs]

/**************************************************************************************/
/*                                                                                    */
/*  Copyright (c) 2005-2011, Michele Bosi.                                            */
/*  All rights reserved.                                                              */
/*                                                                                    */
/*  This file is part of Visualization Library                                        */
/*  http://visualizationlibrary.org                                                   */
/*                                                                                    */
/*  Released under the OSI approved Simplified BSD License                            */
/*  http://www.opensource.org/licenses/bsd-license.php                                */
/*                                                                                    */
/**************************************************************************************/

// This shader maps the volume value to the transfer function plus computes the
// gradient and lighting on the fly. This shader is to be used with IF_LUMINANCE
// image volumes.

#define LIGHTING_ALPHA_THRESHOLD 0.02

varying vec3 frag_position;     // in object space
uniform sampler3D volume_texunit;
uniform sampler3D gradient_texunit;
uniform sampler1D trfunc_texunit;
uniform float trfunc_delta;
uniform vec3 light_position[4]; // light positions in object space
uniform bool light_enable[4];   // light enable flags
uniform vec3 eye_position;      // camera position in object space
uniform float val_threshold;
uniform vec3 gradient_delta; // for on-the-fly gradient computation
uniform bool precomputed_gradient; // whether the gradient has been precomputed or not

// computes a simplified lighting equation
vec3 blinn_phong(vec3 N, vec3 V, vec3 L, int light)
{
    // material properties
    // you might want to put this into a bunch or uniforms
    vec3 Ka = vec3(1.0, 1.0, 1.0);
    vec3 Kd = vec3(1.0, 1.0, 1.0);
    vec3 Ks = vec3(1.0, 1.0, 1.0);
    float shininess = 50.0;

    // diffuse coefficient
    float diff_coeff = max(dot(L,N),0.0);

    // specular coefficient
    vec3 H = normalize(L+V);
    float spec_coeff = pow(max(dot(H,N), 0.0), shininess);
    if (diff_coeff <= 0.0) 
        spec_coeff = 0.0;

    // final lighting model
    return  Ka * gl_LightSource[light].ambient.rgb + 
            Kd * gl_LightSource[light].diffuse.rgb  * diff_coeff + 
            Ks * gl_LightSource[light].specular.rgb * spec_coeff ;
}

void main(void)
{
    // sample the LUMINANCE value
    float val = texture3D(volume_texunit, gl_TexCoord[0].xyz ).r;
    
    // all the pixels whose val is less than val_threshold are discarded
    if (val < val_threshold)
        discard;
    
    // sample the transfer function
    
    // to properly sample the texture clamp bewteen trfunc_delta...1.0-trfunc_delta
    float clamped_val = trfunc_delta+(1.0-2.0*trfunc_delta)*val;
    vec4 color = texture1D(trfunc_texunit, clamped_val);
    vec3 color_tmp = vec3(0.0, 0.0, 0.0);

    // compute the gradient and lighting only if the pixel is visible "enough"
    if (color.a > LIGHTING_ALPHA_THRESHOLD)
    {
        vec3 N;
        if (precomputed_gradient)
        {
            // retrieve pre-computed gradient
            N  = normalize( (texture3D(gradient_texunit, gl_TexCoord[0].xyz).xyz - vec3(0.5,0.5,0.5))*2.0 );
        }
        else
        {
            // on-the-fly gradient computation: slower but requires less memory (no gradient texture required).
            vec3 sample1, sample2;
            sample1.x = texture3D(volume_texunit, gl_TexCoord[0].xyz-vec3(gradient_delta.x,0.0,0.0) ).r;
            sample2.x = texture3D(volume_texunit, gl_TexCoord[0].xyz+vec3(gradient_delta.x,0.0,0.0) ).r;
            sample1.y = texture3D(volume_texunit, gl_TexCoord[0].xyz-vec3(0.0,gradient_delta.y,0.0) ).r;
            sample2.y = texture3D(volume_texunit, gl_TexCoord[0].xyz+vec3(0.0,gradient_delta.y,0.0) ).r;
            sample1.z = texture3D(volume_texunit, gl_TexCoord[0].xyz-vec3(0.0,0.0,gradient_delta.z) ).r;
            sample2.z = texture3D(volume_texunit, gl_TexCoord[0].xyz+vec3(0.0,0.0,gradient_delta.z) ).r;
            N  = normalize( sample1 - sample2 );
        }

        vec3 V  = normalize(eye_position - frag_position);
        for(int i=0; i<4; ++i)
        {
            if (light_enable[i])
            {
                vec3 L = normalize(light_position[i] - frag_position);
                color_tmp.rgb += color.rgb * blinn_phong(N,V,L,i);
            }
        }
    }
    else
        color_tmp = color.rgb;

    gl_FragColor = vec4(color_tmp,color.a);
}
// Have fun!


Visualization Library v1.0.3 Reference Documentation
Copyright Michele Bosi. All rights reserved.
Updated on Tue Feb 7 2017 00:55:05.
Permission is granted to use this page to write and publish articles regarding Visualization Library.