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. The mouse wheel is used to modifies the rendering based on the specific technique.
Essentally this technique renders a cube with 3d texture coordinates, that is, at each corner of the cube corresponds a corner of the volume texture. We then generate for each interpolated texel the xyz objects space position, to do so we pass such position from the vertex shader to the fragment shader using a 'varying' variable. Once we have such position in the fragment shader we subtract the object space eye position to retrieve the ray vector that goes from the center of the camera through the current fragment. Such vector can then be used to start traversing the volume texture. The starting point of such traversal, in texture coordinates, is simply the 3d texture coordinate of the current fragment. We then iterate through the ray using the particular volume visualization tecnique until we reach one of the borders of the 3D texture.
Note that some tecniques require a front to back ray casting (such as isosuface), some others benefit from a back to front ray casting (integration based methods) and for some others the ray casting direction is indifferent (such as for MIP). To raycast back to front we render the back facing polygons of the cube (so that we start from the furthest away texels) and compute the ray direction as eye.xyz - fragment.xyz. On the other hand, to raycast front to back, we render the front facing polygons of the cube (so that we start from the texels closest to the camera) and compute the ray direction as fragment.xyz - eye.xyz (i.e. we invert the direction).
That's it! The actual code implementing the bare bone infrastructure is surprisingly simple and small, all the rest is glue code and ancillary logic.
#include "BaseDemo.hpp"
class App_VolumeRaycast: public BaseDemo
{
float SAMPLE_STEP;
enum RaycastMode {
Isosurface_Mode,
Isosurface_Transp_Mode,
MIP_Mode,
RaycastBrightnessControl_Mode,
RaycastDensityControl_Mode,
RaycastColorControl_Mode
} MODE;
bool DYNAMIC_LIGHTS;
bool COLORED_LIGHTS;
bool PRECOMPUTE_GRADIENT;
public:
{
return BaseDemo::appletInfo() +
"- Left/Right Arrow: change raycast technique.\n" +
"- Up/Down Arrow: changes SAMPLE_STEP.\n" +
"- L: toggles lights (useful only for isosurface).\n" +
"- Mouse Wheel: change the bias used to render the volume.\n" +
"\n" +
"- Drop inside the window a set of 2D files or a DDS or DAT volume to display it.\n" +
"\n";
}
App_VolumeRaycast()
{
SAMPLE_STEP = 512.0f;
MODE = Isosurface_Mode;
DYNAMIC_LIGHTS = false;
COLORED_LIGHTS = false;
PRECOMPUTE_GRADIENT = false;
}
virtual void initEvent()
{
{
exit(1);
}
rendering()->
as<
Rendering>()->transform()->addChild( mLight0Tr.get() );
rendering()->
as<
Rendering>()->transform()->addChild( mLight1Tr.get() );
rendering()->
as<
Rendering>()->transform()->addChild( mLight2Tr.get() );
mValThreshold =
new Uniform(
"val_threshold" );
mValThreshold->setUniformF( 0.5f );
mVolumeImage =
loadImage(
"/volume/VLTest.dat" );
setupScene();
}
void setupScene()
{
sceneManager()->tree()->eraseAllChildren();
sceneManager()->tree()->actors()->clear();
mLight0->bindTransform(
NULL );
mLight1->bindTransform(
NULL );
mLight2->bindTransform(
NULL );
if ( MODE == RaycastBrightnessControl_Mode || MODE == RaycastDensityControl_Mode || MODE == RaycastColorControl_Mode )
{
}
mRaycastVolume->lights().push_back( mLight0 );
if ( DYNAMIC_LIGHTS )
{
if ( COLORED_LIGHTS )
{
mLight0->setAmbient(
fvec4( 0.1f, 0.1f, 0.1f, 1.0f ) );
mLight1->setAmbient(
fvec4( 0.1f, 0.1f, 0.1f, 1.0f ) );
mLight2->setAmbient(
fvec4( 0.1f, 0.1f, 0.1f, 1.0f ) );
mLight0->setDiffuse( vl::gold );
mLight1->setDiffuse( vl::green );
mLight2->setDiffuse( vl::royalblue );
}
mRaycastVolume->lights().push_back( mLight1 );
mRaycastVolume->lights().push_back( mLight2 );
mLight0->bindTransform( mLight0Tr.get() );
mLight1->bindTransform( mLight1Tr.get() );
mLight2->bindTransform( mLight2Tr.get() );
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() );
}
mGLSL->attachShader(
new GLSLVertexShader(
"/glsl/volume_luminance_light.vs" ) );
if ( MODE == Isosurface_Mode )
else
if ( MODE == Isosurface_Transp_Mode )
mGLSL->attachShader(
new GLSLFragmentShader(
"/glsl/volume_raycast_isosurface_transp.fs" ) );
else
if ( MODE == MIP_Mode )
else
if ( MODE == RaycastBrightnessControl_Mode )
else
if ( MODE == RaycastDensityControl_Mode )
else
if ( MODE == RaycastColorControl_Mode )
trackball()->setTransform( mVolumeTr.get() );
mVolumeAct->setEffect( volume_fx.
get() );
mVolumeAct->setTransform( mVolumeTr.get() );
sceneManager()->tree()->addActor( mVolumeAct.get() );
mVolumeAct->setUniform( mValThreshold.get() );
mRaycastVolume->bindActor( mVolumeAct.get() );
AABB volume_box(
vec3( -10,-10,-10 ),
vec3( +10,+10,+10 ) );
mRaycastVolume->setBox( volume_box );
mValThresholdText =
new Text;
mValThresholdText->translate( 0,5,0 );
mValThresholdText->setBackgroundEnabled( true );
mValThresholdText->setBackgroundColor(
fvec4( 0,0,0,0.75 ) );
mValThresholdText->setColor( vl::white );
updateText();
sceneManager()->tree()->addActor( mValThresholdText.get(), effect.
get() );
}
setupVolume();
}
void setupVolume()
{
Effect* volume_fx = mVolumeAct->effect();
if ( PRECOMPUTE_GRADIENT )
{
}
Log::debug(
Say(
"Volume: %n %n %n\n") << mVolumeImage->width() << mVolumeImage->height() << mVolumeImage->depth() );
mRaycastVolume->generateTextureCoordinates(
ivec3( mVolumeImage->width(), mVolumeImage->height(), mVolumeImage->depth() ) );
if ( COLORED_LIGHTS && DYNAMIC_LIGHTS ) {
}
else {
}
if ( MODE == Isosurface_Mode || MODE == Isosurface_Transp_Mode )
{
if ( PRECOMPUTE_GRADIENT )
{
}
}
updateText();
openglContext()->update();
}
void fileDroppedEvent( const std::vector<String>& files )
{
if( files.size() == 1 )
{
if ( files[0].endsWith( ".dat" ) || files[0].endsWith( ".dds" ) || files[0].endsWith( ".mhd" ) )
{
float h_start = -1000;
float h_end = +1500;
for(int i=0; i<mVolumeImage->width() * mVolumeImage->height() * mVolumeImage->depth(); ++i, ++ival, ++uval) {
float t = ( (*ival) - h_start ) / ( h_end - h_start );
*uval = (
u16)( t * 65535 );
}
}
if ( mVolumeImage ) {
setupVolume();
}
}
}
else
{
std::vector<String> files_sorted = files;
std::sort( files_sorted.begin(), files_sorted.end() );
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();
}
if ( mVolumeImage )
setupVolume();
}
if ( !mVolumeImage )
}
void updateText()
{
switch(MODE)
{
case Isosurface_Mode: technique_name = "raycast isosurface >"; break;
case Isosurface_Transp_Mode: technique_name = "< raycast transparent isosurface >"; break;
case MIP_Mode: technique_name = "< raycast maximum intensity projection >"; break;
case RaycastBrightnessControl_Mode: technique_name = "< raycast brightness control >"; break;
case RaycastDensityControl_Mode: technique_name = "< raycast density control >"; break;
case RaycastColorControl_Mode: technique_name = "< raycast color control"; break;
};
float val_threshold = 0;
mValThreshold->getUniform( &val_threshold );
mValThresholdText->setText(
Say(
"val_threshold = %n\n" "sample_step = 1.0 / %.0n\n" "%s" ) << val_threshold << SAMPLE_STEP << technique_name );
}
void updateValThreshold( int val )
{
float val_threshold = 0.0f;
mValThreshold->getUniform( &val_threshold );
val_threshold += val * 0.01f;
val_threshold =
clamp( val_threshold, 0.0f, 1.0f );
mValThreshold->setUniformF( val_threshold );
updateText();
openglContext()->update();
}
void mouseWheelEvent( int val )
{
updateValThreshold( val );
}
virtual void updateScene()
{
if ( DYNAMIC_LIGHTS )
{
mLight0Tr->setLocalMatrix( mat );
mLight1Tr->setLocalMatrix( mat );
mLight2Tr->setLocalMatrix( mat );
}
}
virtual void keyPressEvent(
unsigned short,
EKey key)
{
RaycastMode modes[] = { Isosurface_Mode, Isosurface_Transp_Mode, MIP_Mode, RaycastBrightnessControl_Mode, RaycastDensityControl_Mode, RaycastColorControl_Mode };
int mode = MODE;
mode++;
else
mode--;
{
SAMPLE_STEP += 64;
}
else
{
SAMPLE_STEP -= 64;
}
{
if (!DYNAMIC_LIGHTS)
{
DYNAMIC_LIGHTS = true;
COLORED_LIGHTS = false;
}
else
if (DYNAMIC_LIGHTS && !COLORED_LIGHTS)
{
DYNAMIC_LIGHTS = true;
COLORED_LIGHTS = true;
}
else
{
DYNAMIC_LIGHTS = false;
COLORED_LIGHTS = false;
}
}
setupScene();
}
virtual void destroyEvent() {
BaseDemo::destroyEvent();
mLight0 = mLight1 = mLight2 =
NULL;
}
private:
};