# Visualization Library

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

[Home] [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.

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::print(appletInfo());

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

        // lights to be used later
        mLight0 = new Light(0);
        mLight1 = new Light(1);
        mLight2 = new Light(2);

        // 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->followTransform( mLight0Tr.get() );
          mLight1->followTransform( mLight1Tr.get() );
          mLight2->followTransform( 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() );
        // add the other lights only if dynamic lights have to be displayed
        if (DYNAMIC_LIGHTS)
        {
          vol_fx->shader()->setRenderState( mLight1.get() );
          vol_fx->shader()->setRenderState( mLight2.get() );
        }

        // 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);
        ref<Geometry> box_outline = vl::makeBox(volume_box);
        box_outline->setColor(vl::red);
        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_TextureUnit0);
        // remove texture unit #1 and #2
        vol_fx->shader()->eraseRenderState(RS_TextureUnit1);
        vol_fx->shader()->eraseRenderState(RS_TextureUnit2);

        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::info("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()->gocTextureUnit(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()->gocTextureUnit(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()->gocTextureUnit(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::info("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()->gocTextureUnit(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::info("Non IF_LUMINANCE image: not using GLSL.\n");
          // install volume texture
          vol_fx->shader()->gocTextureUnit(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`]

    /*  http://www.visualizationlibrary.com                                               */
    /*                                                                                    */
    /*  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`]

    /*  http://www.visualizationlibrary.com                                               */
    /*                                                                                    */
    /*  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;

        // 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 v2011.05.1142 Reference Documentation
Copyright 2005-2011 Michele Bosi. All rights reserved.
Updated on Thu May 26 2011 22:51:30.
Permission is granted to use this page to write and publish articles regarding Visualization Library.