How to improve my volume rendering ?

Hi guys !

I’m working on a small program, a viewer for 3D cube objects more precisely.
The data I get is a 1D array of floats and a header for describing the dimension of the cube.

First, I plotted each points in the graphic space, and each points are black with an alpha value related to the 1D array corresponding value. Of course, OpenGL hates plotting points.

So I finally used Vertex array and the function glDrawArrays to draw quads. I stack these quads and apply to each quads a RGBA texture. I got the wanted rendering but the perfs are too bad : 1 FPS, and I wanted to reach 10 FPS.

But I don’t have any idea to improve the performance of my volume rendering.

Here is the rendering to see what I want to get (and what I got): (It’s a tangerine from a commercial X-ray scanner)

Here’s my code…
Everything is in here, except how to read the .r3d file but you don’t need.
The volume is typically 350350500 of size. (500 slices, with 350*350 textures).
Also, I apologise for the lack of comments but this program was not mean to but a long and taugh work.

#define STRICT
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <stdlib.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
#include "resource.h"
#include "FichierR3D.h"

HWND   g_hWnd      = NULL;
HDC    g_hDC       = NULL;
HGLRC  g_hRC       = NULL;
GLuint *g_textureID = NULL;

bool g_bBlendOutColorKey = true;

float g_fSpinX = 0.0f;
float g_fSpinY = 0.0f;
float g_zoom = -10.0f;

CFichierR3D fich;
R3D_HEADER header;
float* volume;

unsigned char *pImage_RGBA = NULL;

struct Vertex
{
	float tu, tv;
	float x, y, z;
};
Vertex *g_quadVertices;


int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance, 
				   LPSTR lpCmdLine, int nCmdShow);
LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
void loadTexture(void);
void init(void);
void render(void);
void shutDown(void);


int WINAPI WinMain( HINSTANCE hInstance,
				   HINSTANCE hPrevInstance,
				   LPSTR     lpCmdLine,
				   int       nCmdShow )
{
	WNDCLASSEX winClass; 
	MSG        uMsg;

	memset(&uMsg,0,sizeof(uMsg));

	winClass.lpszClassName = "MY_WINDOWS_CLASS";
	winClass.cbSize        = sizeof(WNDCLASSEX);
	winClass.style         = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	winClass.lpfnWndProc   = WindowProc;
	winClass.hInstance     = hInstance;
	winClass.hIcon         = LoadIcon(hInstance, (LPCTSTR)IDI_OPENGL_ICON);
	winClass.hIconSm       = LoadIcon(hInstance, (LPCTSTR)IDI_OPENGL_ICON);
	winClass.hCursor       = LoadCursor(NULL, IDC_ARROW);
	winClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	winClass.lpszMenuName  = NULL;
	winClass.cbClsExtra    = 0;
	winClass.cbWndExtra    = 0;

	if( !RegisterClassEx(&winClass) )
		return E_FAIL;

	g_hWnd = CreateWindowEx( NULL, "MY_WINDOWS_CLASS", 
		"OpenGL - Color Key Transparency",
		WS_OVERLAPPEDWINDOW | WS_VISIBLE,
		0, 0, 640, 480, NULL, NULL, hInstance, NULL );

	if( g_hWnd == NULL )
		return E_FAIL;

	ShowWindow( g_hWnd, nCmdShow );
	UpdateWindow( g_hWnd );

	init();

	while( uMsg.message != WM_QUIT )
	{
		if( PeekMessage( &uMsg, NULL, 0, 0, PM_REMOVE ) )
		{
			TranslateMessage( &uMsg );
			DispatchMessage( &uMsg );
		}
		else
			render();
	}

	shutDown();

	UnregisterClass( "MY_WINDOWS_CLASS", hInstance );

	return uMsg.wParam;
}



LRESULT CALLBACK WindowProc( HWND   hWnd, 
							UINT   msg, 
							WPARAM wParam, 
							LPARAM lParam )
{
	static POINT ptLastMousePosit;
	static POINT ptCurrentMousePosit;
	static bool bMousing;

	switch( msg )
	{
	case WM_KEYDOWN:
		{
			switch( wParam )
			{
			case VK_ESCAPE:
				PostQuitMessage(0);
				break;

			case VK_F1:
				g_bBlendOutColorKey = !g_bBlendOutColorKey;
				break;

			case VK_UP:
				g_zoom = g_zoom + 0.5f;
				break;

			case VK_DOWN:
				g_zoom = g_zoom - 0.5f;
				break;
			}
		}
		break;

	case WM_LBUTTONDOWN:
		{
			ptLastMousePosit.x = ptCurrentMousePosit.x = LOWORD (lParam);
			ptLastMousePosit.y = ptCurrentMousePosit.y = HIWORD (lParam);
			bMousing = true;
		}
		break;

	case WM_LBUTTONUP:
		{
			bMousing = false;
		}
		break;

	case WM_MOUSEMOVE:
		{
			ptCurrentMousePosit.x = LOWORD (lParam);
			ptCurrentMousePosit.y = HIWORD (lParam);

			if( bMousing )
			{
				g_fSpinX -= (ptCurrentMousePosit.x - ptLastMousePosit.x);
				g_fSpinY -= (ptCurrentMousePosit.y - ptLastMousePosit.y);
			}

			ptLastMousePosit.x = ptCurrentMousePosit.x;
			ptLastMousePosit.y = ptCurrentMousePosit.y;
		}
		break;

	case WM_SIZE:
		{
			int nWidth  = LOWORD(lParam); 
			int nHeight = HIWORD(lParam);
			glViewport(0, 0, nWidth, nHeight);

			glMatrixMode( GL_PROJECTION );
			glLoadIdentity();
			gluPerspective( 45.0, (GLdouble)nWidth / (GLdouble)nHeight, 0.1, 100.0);
		}
		break;

	case WM_CLOSE:
		{
			PostQuitMessage(0); 
		}

	case WM_DESTROY:
		{
			PostQuitMessage(0);
		}
		break;

	default:
		{
			return DefWindowProc( hWnd, msg, wParam, lParam );
		}
		break;
	}

	return 0;
}

void loadTexture(int s)    
{
	int i,j;
	for( i = 0, j = 0 ; i < header.sliceHeight * header.sliceWidth*4; i += 4, j++)
	{
		pImage_RGBA[i]   = 0;
		pImage_RGBA[i+1] = 0;
		pImage_RGBA[i+2] = 0;
		pImage_RGBA[i+3] = (unsigned char)(volume[s * header.sliceHeight * header.sliceWidth + j]*10);
	}

	glBindTexture( GL_TEXTURE_2D, g_textureID[s] );
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_NEAREST);

	glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, header.sliceHeight, header.sliceWidth, 0,
		GL_RGBA, GL_UNSIGNED_BYTE, pImage_RGBA);
}

void init( void )
{
	GLuint PixelFormat;

	PIXELFORMATDESCRIPTOR pfd;
	memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));

	pfd.nSize      = sizeof(PIXELFORMATDESCRIPTOR);
	pfd.nVersion   = 1;
	pfd.dwFlags    = PFD_DRAW_TO_WINDOW |PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
	pfd.iPixelType = PFD_TYPE_RGBA;
	pfd.cColorBits = 16;
	pfd.cDepthBits = 16;

	g_hDC = GetDC( g_hWnd );
	PixelFormat = ChoosePixelFormat( g_hDC, &pfd );
	SetPixelFormat( g_hDC, PixelFormat, &pfd);
	g_hRC = wglCreateContext( g_hDC );
	wglMakeCurrent( g_hDC, g_hRC );

	//--------------------------------------------------
	g_textureID = new GLuint[header.numberOfSlices];
	glGenTextures( header.numberOfSlices, g_textureID );

	char *FileName = "C:/mandarine.r3d";
	fich.Open(FileName);
	fich.GetHeader(&header);
	volume = new float[header.numberOfSlices * header.sliceHeight * header.sliceWidth];
	fich.ReadVolume(volume);

	pImage_RGBA = new unsigned char[header.sliceHeight * header.sliceWidth * 4]; 

	float min = volume[0], max = volume[0];
	for (int k = 1; k < header.numberOfSlices * header.sliceHeight * header.sliceWidth; k++)
	{
		if (min > volume[k])
			min = volume[k];
		else if (max < volume[k])
			max = volume[k];
	}
	for (int k = 0; k < header.numberOfSlices * header.sliceHeight * header.sliceWidth; k++)
		volume[k] = (volume[k] - min) / (max - min);
	//--------------------------------------------------

	glClearColor( 1.0f, 1.0f, 1.0f, 1.0f );
	glEnable(GL_TEXTURE_2D);

	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
	gluPerspective( 45.0f, 640.0f / 480.0f, 0.1f, 100.0f);

	int i,j;
	g_quadVertices = new Vertex[4*header.numberOfSlices];

	for (i = 0 , j = 0 ; j < header.numberOfSlices ; i += 4, j++)
	{
		g_quadVertices[i].tu = 0.0f;
		g_quadVertices[i].tv = 0.0f;
		g_quadVertices[i].x = -1.0f;
		g_quadVertices[i].y = -1.0f;
		g_quadVertices[i].z = (float)(-header.numberOfSlices/2 + j)/120.0f;

		g_quadVertices[i+1].tu = 1.0f;
		g_quadVertices[i+1].tv = 0.0f;
		g_quadVertices[i+1].x = 1.0f;
		g_quadVertices[i+1].y = -1.0f;
		g_quadVertices[i+1].z = (float)(-header.numberOfSlices/2 + j)/120.0f;

		g_quadVertices[i+2].tu = 1.0f;
		g_quadVertices[i+2].tv = 1.0f;
		g_quadVertices[i+2].x = 1.0f;
		g_quadVertices[i+2].y = 1.0f;
		g_quadVertices[i+2].z = (float)(-header.numberOfSlices/2 + j)/120.0f;

		g_quadVertices[i+3].tu = 0.0f;
		g_quadVertices[i+3].tv = 1.0f;
		g_quadVertices[i+3].x = -1.0f;
		g_quadVertices[i+3].y = 1.0f;
		g_quadVertices[i+3].z = (float)(-header.numberOfSlices/2 + j)/120.0f;
	}
}

void shutDown( void )   
{
	glDeleteTextures( header.numberOfSlices, g_textureID );
	delete[] g_quadVertices;
	delete[] pImage_RGBA;

	if( g_hRC != NULL )
	{
		wglMakeCurrent( NULL, NULL );
		wglDeleteContext( g_hRC );
		g_hRC = NULL;
	}

	if( g_hDC != NULL )
	{
		ReleaseDC( g_hWnd, g_hDC );
		g_hDC = NULL;
	}
}

void render( void )
{
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
	glTranslatef( 0.0f, 0.0f, g_zoom );
	glRotatef( -g_fSpinY, 1.0f, 0.0f, 0.0f );
	glRotatef( -g_fSpinX, 0.0f, 1.0f, 0.0f );

	if( g_bBlendOutColorKey == true )
	{
		glEnable( GL_BLEND );
		glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
	}

	glInterleavedArrays( GL_T2F_V3F, 0, g_quadVertices );

	int s;
	for (s = 0 ; s < header.numberOfSlices ; s ++)
	{
		loadTexture(s);
		glBindTexture( GL_TEXTURE_2D, g_textureID[s] );
		glDrawArrays( GL_QUADS, s*4, 4 );
	}

	glDisable(GL_BLEND);

	SwapBuffers( g_hDC );
}

The intereting part is in render and loadtexture.

Edit:// Of course, I tried to load all the texture at the same time and stored all the pointers to these textures in g_textureID[s].
But I need 350350500*4 bytes = 245 MB on the graphic card, which I don’t have (128 MB on the device). Actually, the program crash after having loaded the 200th slice (and I’m 100% sure it’s because memory’s full).

Any idea ? I heard about pixel shader or pixel buffer, but I don’t know if it will help me.

Without looking to your code:

Are you using 500 different textures? If so: put multiple textures in one large texture and select the correct slice using the proper texture coordinates. For example, if you are using a 4096x4096 texture (which is likely to be the maximum supported texture size), you can store:

4096 / 350 = 11.7 -> 11 x 11 = 121 slices in one texture.

This means you only have to bind 1 texture for 121 slices.
If you can support OpenGL 3, you could also use texture arrays to create a single texture array of 350x350 with 500 slices in the texture array (not sure about size limitations).

If you only have 128mb of memory on your graphics card, I’m not sure what the best strategy is for storing this many data. You could use a couple of 4096x4096 textures or a couple of texture arrays (OpenGL 3.0 and up, and I believe there is an extension as well for OpenGL 2.1) of say: 100 slices each. It does mean that the textures have to be loaded on each frame because there isn’t enough room on your graphics card.

Another option might be to use texture compression. But I’m not sure how much compression you would be able to achieve.

Thanks,

Somebody on an other forum just told me to compress my textures.
I guess combining textures compression and textures gathering I could get some more FPS.

Thanks for the tips, I wasn’t able to get the idea alone.

I keep you posted.

You’re welcome. Storing many textures in one large texture helps because it seriously limits the number of bind calls (which take lots of cpu time).

I’m curious to hear what the increase in performance will be when you implemented these things, so please keep us updated.

The standard way for volume rendering is to use one 3D texture. In your code, you use nearest interpolation so it is not harmful right now but if you decide to switch to linear interpolation, with 2D textures, you lose interpolation of values between slices.

And with one 3D texture, you don’t need to change texture binding.

Also, you are storing your one-component dataset into RGBA textures and only uses the alpha component. You are wasting a lot of VRAM and probably kill texture caching at the same time, which can have a performance impact.

Compressing the textures is totally useless, unfortunately, even (of course) after having loaded all the textures into the memory.

But, Gathering some textures into only one textures(that induces less loadtextures() calls) is effective. it’s like 2 FPS now.

But it seems this is not at all the right things to do to render a volume, even if some commercial software (like amira) use this method.

So I guess I need to do something else from scratch, like 3D textures or pixels buffer.

So, apart from plotting quad slices with textures, what would you do to render this tangerine into a 3D space with OpenGL ?

Did you already try overlay his suggestion? You only have to store the alpha value, because the r, g, b colours you put in are just 0, 0, 0. If you create your own simple fragment shader that reads the alpha value from a texture, the fragment shader can fill in the r,g,b values (0, 0, 0 in your case).

This means you only need 1 x 350 x 350 x 500 ~= 60mb of memory on your graphics card.

And after looking to your code:
You are calling loadtexture for each frame you are rendering. Thatis not necessary. After you have loaded a texture, you only have to bind the texture to use it. So in your render function remove the loadTexture call (and put it into an initialization part of your code).

So, I tried to use Luminance-Alpha instead of RGBA.

void loadTexture(int s)    
{
	int i,j;
	for( i = 0, j = 0 ; i < header.sliceHeight * header.sliceWidth*2; i += 2, j++)
	{
		pImage_RGBA[i]   = 0;
		pImage_RGBA[i+1] = (unsigned char)(volume[s * header.sliceHeight * header.sliceWidth + j]*10);
	}

	glBindTexture( GL_TEXTURE_2D, g_textureID[s] );
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_NEAREST);

	glTexImage2D( GL_TEXTURE_2D, 0, 2, header.sliceHeight, header.sliceWidth, 0,
		GL_RGBA, GL_UNSIGNED_BYTE, pImage_RGBA);
}

But I have to use GL_RGBA in pixel target in the function glTexImage2D. If not, textures are totally random. It seems Windows just copy luminance into RGB values.
Have a look here : glTexImage2D function (Gl.h) - Win32 apps | Microsoft Learn

Each element is a luminance/alpha pair. It is converted to floating point, and then assembled into an RGBA element by replicating the luminance value three times for red, green, and blue. Each component is then multiplied by the signed scale factor GL_c_SCALE, added to the signed bias GL_c_BIAS, and clamped to the range [0,1] (see glPixelTransfer).

However I need to do :

pImage_RGBA = new unsigned char[header.sliceHeight * header.sliceWidth * 4];

instead of (*2), because it will pop a access violation.
So unfortunately, I can’t reduce by 4 the size of the textures.

In addition, I put this into init() :

	int s;
	for ( s = 0 ; s < header.numberOfSlices ; s ++)
	{
		loadTexture(s);
	}

And this into the render function:

	int s;
	for (s = 0 ; s < header.numberOfSlices ; s ++)
	{
		glBindTexture( GL_TEXTURE_2D, g_textureID[s] );
		glDrawArrays( GL_QUADS, s*4, 4 );
	}

But the result is totally crappy:

But ! There’s no more lag (35 FPS).

It seems pointers to textures are lost. But I don’t know. Are datas provided to glTexImage2D are used after this function ?

All data given to glTexImage2D can be deleted after the texture is created. The texture stays in memory (main memory or on the graphics card, the graphics driver decides) as long as you don’t call glDeleteTextures for the specific texture.

Why not use `GL_ALPHA’ as texture format and create a fragment shader program to set the output colour?

The following is all from the top of my head, untested, so it will probably contain some bugs and does not work straight away. But it should give you the general idea. The code below does not include putting all textures into a single texture. But if you manage to use textures only containing an alpha value I suggest you create a 3d texture containing all slices (as overlay suggested) so you have one big texture that only has to be bound a single time when rendering.


// extra/changed global vars
unsigned char *pImage_ALPHA = NULL;
//GLint vertexShader = 0; // might not be necessary, if I recall correctly a default vertex shader program is used if you don't attach one to a glsl program
GLint fragmentShader = 0;
GLint shaderProgram = 0;

// your loadTextures function
void loadTexture(int s)    
{
  for(int i = 0; i < header.sliceHeight * header.sliceWidth; ++i)
  {
    // why multiply by 10 btw?
    pImage_ALPHA[i] = volume[s * header.sliceHeight * header.sliceWidth + i] * 10; // no need to convert to unsigned byte (see below)
  }

  glBindTexture( GL_TEXTURE_2D, g_textureID[s] );
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_NEAREST);

  glTexImage2D(
    GL_TEXTURE_2D,
    0,
    GL_ALPHA, // GL_ALPHA instead of GL_RGBA
    header.sliceHeight,
    header.sliceWidth,
    0,
    GL_ALPHA, // GL_ALPHA instead of GL_RGBA
    GL_BYTE, // use GL_BYTE instead of GL_UNSIGNED_BYTE
    pImage_ALPHA);
}



// use custom fragment shader, call this function when initializing the program
void initShader()
{
  // create a shader
  fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

  // variable that holds the source of the fragment shader
  GLchar const src[] =
    "#version 120
 \
     uniform sampler2D alpha_texture;
 \
     varying vec2 texcoord;
 \
     
 \
     void main(void)
 \
     {
 \
       // do a texture lookup to obtain the alpha value 
\
       float alpha = texture(alpha_texture, texcoord); 
\
       // set the output colour to black with alpha 
\
       gl_FragColor = vec4(0.0, 0.0, 0.0, alpha);
 \
     }
";

  // add the source to the shader
  glShaderSource(fragmentShader, 1, &src, 0);

  // compile the shader
  glCompileShader(fragmentShader);

  // should check here for compilation status with
  // glGetShaderiv and GL_COMPILE_STATUS
  // if compiling failed, you can get an error log with
  // glGetShaderInfoLog

  // create a program
  shaderProgram = glCreateProgram();

  // attach the fragment shader to the program
  glAttachShader(shaderProgram, fragmentShader);

  // link the program
  glLinkProgram(shaderProgram);

  // you should now check for linking status with
  // glGetProgramiv and GL_LINK_STATUS
  // an error log can be obtained with
  // glGetProgramInfoLog
}



// your render function must use the custom shader program
void render( void )
{
  // make sure the custom shader program is used
  glUseProgram(shaderProgram);

  
  // rest of the code stays the same...


  // if you want to go back to the default shader program in the end
  // glUseProgram(0);

>It seems pointers to textures are lost.
Be sure to init this array correctly :
g_textureID[s]

Can you show the code about it ?

Yeah, it’s done in the init() function.

void init( void )
{
	GLuint PixelFormat;

	PIXELFORMATDESCRIPTOR pfd;
	memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));

	pfd.nSize      = sizeof(PIXELFORMATDESCRIPTOR);
	pfd.nVersion   = 1;
	pfd.dwFlags    = PFD_DRAW_TO_WINDOW |PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
	pfd.iPixelType = PFD_TYPE_RGBA;
	pfd.cColorBits = 16;
	pfd.cDepthBits = 16;

	g_hDC = GetDC( g_hWnd );
	PixelFormat = ChoosePixelFormat( g_hDC, &pfd );
	SetPixelFormat( g_hDC, PixelFormat, &pfd);
	g_hRC = wglCreateContext( g_hDC );
	wglMakeCurrent( g_hDC, g_hRC );

	//--------------------------------------------------
	g_textureID = new GLuint[header.numberOfSlices];
	glGenTextures( header.numberOfSlices, g_textureID );

	char *FileName = "C:/mandarine.r3d";
	fich.Open(FileName);
	fich.GetHeader(&header);
	volume = new float[header.numberOfSlices * header.sliceHeight * header.sliceWidth];
	fich.ReadVolume(volume);

	pImage_RGBA = new unsigned char[header.sliceHeight * header.sliceWidth * 4]; 

	float min = volume[0], max = volume[0];
	for (int k = 1; k < header.numberOfSlices * header.sliceHeight * header.sliceWidth; k++)
	{
		if (min > volume[k])
			min = volume[k];
		else if (max < volume[k])
			max = volume[k];
	}
	for (int k = 0; k < header.numberOfSlices * header.sliceHeight * header.sliceWidth; k++)
		volume[k] = (volume[k] - min) / (max - min);
	//--------------------------------------------------

	glClearColor( 1.0f, 1.0f, 1.0f, 1.0f );
	glEnable(GL_TEXTURE_2D);

	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
	gluPerspective( 45.0f, 640.0f / 480.0f, 0.1f, 100.0f);

	int i,j;
	g_quadVertices = new Vertex[4*header.numberOfSlices];

	for (i = 0 , j = 0 ; j < header.numberOfSlices ; i += 4, j++)
	{
		g_quadVertices[i].tu = 0.0f;
		g_quadVertices[i].tv = 0.0f;
		g_quadVertices[i].x = -1.0f;
		g_quadVertices[i].y = -1.0f;
		g_quadVertices[i].z = (float)(-header.numberOfSlices/2 + j)/120.0f;

		g_quadVertices[i+1].tu = 1.0f;
		g_quadVertices[i+1].tv = 0.0f;
		g_quadVertices[i+1].x = 1.0f;
		g_quadVertices[i+1].y = -1.0f;
		g_quadVertices[i+1].z = (float)(-header.numberOfSlices/2 + j)/120.0f;

		g_quadVertices[i+2].tu = 1.0f;
		g_quadVertices[i+2].tv = 1.0f;
		g_quadVertices[i+2].x = 1.0f;
		g_quadVertices[i+2].y = 1.0f;
		g_quadVertices[i+2].z = (float)(-header.numberOfSlices/2 + j)/120.0f;

		g_quadVertices[i+3].tu = 0.0f;
		g_quadVertices[i+3].tv = 1.0f;
		g_quadVertices[i+3].x = -1.0f;
		g_quadVertices[i+3].y = 1.0f;
		g_quadVertices[i+3].z = (float)(-header.numberOfSlices/2 + j)/120.0f;
	}

	int s;
	for (s = 0 ; s < header.numberOfSlices ; s ++)
	{
		loadTexture(s);
	}
}

Last lines of this code are odd, I know, but I’m currently debugging the app.

Heiko, I’m currently working on what you sent me. Thanks a lot ! I keep you posted, as usual.

Vagdish,

you also should be aware that blending hundreds of slices causes severe artifacts when you’re rendering into an 8-bit color buffer. The blending operation itself is done in floating point, but each result is written into the 8 bits.

Check out this link: http://books.google.de/books?id=H4eYq7-2…;q=&f=false

I solved this by rendering into an off-screen floating point texture. If you don’t have those available, you could try to find some suitable 12 or 16 bit format.

EDIT: Ahh, should have looked at your code before posting - seems you are already using a 16 bit buffer. Well, I guess stressing this once too often doesn’t hurt :slight_smile:

Hi !

Thanks to you, I finally managed to have a decent FPS for my rendering.
Actually, I think it’s more beginner mistakes (I don’t know exactly where) than a lack of memory or the non use of shader or other recent functions.

I wrote the entire code again, using only Alpha value. But it didn’t work. Then I changed Alpha value to RGBA because it seems windows only accept RGBA.

And now it’s working !

10 to 20 FPS !

Here is the entire source code of this “mini viewer engine” for a cube of float.
It’s for you, for comparing with what I’ve done, and for other guys who want to render small volumes easily.

You have to change the code inside init() and loadTexture().

#define STRICT
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <stdlib.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glaux.h>
#include "resource.h"
#include "FichierR3D.h"

HWND   g_hWnd      = NULL;
HDC    g_hDC       = NULL;
HGLRC  g_hRC       = NULL;
GLuint *g_textureID = NULL;

bool g_bBlendOutColorKey = true;

float g_fSpinX = 0.0f;
float g_fSpinY = 0.0f;
float g_zoom = -10.0f;

CFichierR3D fich;
R3D_HEADER header;
float* volume;

unsigned char *pImage_RGBA = NULL;

struct Vertex
{
	float tu, tv;
	float x, y, z;
};
Vertex *g_quadVertices;


int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance, 
				   LPSTR lpCmdLine, int nCmdShow);
LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
void loadTexture(void);
void init(void);
void render(void);
void shutDown(void);


int WINAPI WinMain( HINSTANCE hInstance,
				   HINSTANCE hPrevInstance,
				   LPSTR     lpCmdLine,
				   int       nCmdShow )
{
	WNDCLASSEX winClass; 
	MSG        uMsg;

	memset(&uMsg,0,sizeof(uMsg));

	winClass.lpszClassName = "MY_WINDOWS_CLASS";
	winClass.cbSize        = sizeof(WNDCLASSEX);
	winClass.style         = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	winClass.lpfnWndProc   = WindowProc;
	winClass.hInstance     = hInstance;
	winClass.hIcon         = LoadIcon(hInstance, (LPCTSTR)IDI_OPENGL_ICON);
	winClass.hIconSm       = LoadIcon(hInstance, (LPCTSTR)IDI_OPENGL_ICON);
	winClass.hCursor       = LoadCursor(NULL, IDC_ARROW);
	winClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	winClass.lpszMenuName  = NULL;
	winClass.cbClsExtra    = 0;
	winClass.cbWndExtra    = 0;

	if( !RegisterClassEx(&winClass) )
		return E_FAIL;

	g_hWnd = CreateWindowEx( NULL, "MY_WINDOWS_CLASS", 
		"OpenGL - Color Key Transparency",
		WS_OVERLAPPEDWINDOW | WS_VISIBLE,
		0, 0, 640, 480, NULL, NULL, hInstance, NULL );

	if( g_hWnd == NULL )
		return E_FAIL;

	ShowWindow( g_hWnd, nCmdShow );
	UpdateWindow( g_hWnd );

	init();

	while( uMsg.message != WM_QUIT )
	{
		if( PeekMessage( &uMsg, NULL, 0, 0, PM_REMOVE ) )
		{
			TranslateMessage( &uMsg );
			DispatchMessage( &uMsg );
		}
		else
			render();
	}

	shutDown();

	UnregisterClass( "MY_WINDOWS_CLASS", hInstance );

	return uMsg.wParam;
}



LRESULT CALLBACK WindowProc( HWND   hWnd, 
							UINT   msg, 
							WPARAM wParam, 
							LPARAM lParam )
{
	static POINT ptLastMousePosit;
	static POINT ptCurrentMousePosit;
	static bool bMousing;

	switch( msg )
	{
	case WM_KEYDOWN:
		{
			switch( wParam )
			{
			case VK_ESCAPE:
				PostQuitMessage(0);
				break;

			case VK_F1:
				g_bBlendOutColorKey = !g_bBlendOutColorKey;
				break;

			case VK_UP:
				g_zoom = g_zoom + 0.5f;
				break;

			case VK_DOWN:
				g_zoom = g_zoom - 0.5f;
				break;
			}
		}
		break;

	case WM_LBUTTONDOWN:
		{
			ptLastMousePosit.x = ptCurrentMousePosit.x = LOWORD (lParam);
			ptLastMousePosit.y = ptCurrentMousePosit.y = HIWORD (lParam);
			bMousing = true;
		}
		break;

	case WM_LBUTTONUP:
		{
			bMousing = false;
		}
		break;

	case WM_MOUSEMOVE:
		{
			ptCurrentMousePosit.x = LOWORD (lParam);
			ptCurrentMousePosit.y = HIWORD (lParam);

			if( bMousing )
			{
				g_fSpinX -= (ptCurrentMousePosit.x - ptLastMousePosit.x);
				g_fSpinY -= (ptCurrentMousePosit.y - ptLastMousePosit.y);
			}

			ptLastMousePosit.x = ptCurrentMousePosit.x;
			ptLastMousePosit.y = ptCurrentMousePosit.y;
		}
		break;

	case WM_SIZE:
		{
			int nWidth  = LOWORD(lParam); 
			int nHeight = HIWORD(lParam);
			glViewport(0, 0, nWidth, nHeight);

			glMatrixMode( GL_PROJECTION );
			glLoadIdentity();
			gluPerspective( 45.0, (GLdouble)nWidth / (GLdouble)nHeight, 0.1, 100.0);
		}
		break;

	case WM_CLOSE:
		{
			PostQuitMessage(0); 
		}

	case WM_DESTROY:
		{
			PostQuitMessage(0);
		}
		break;

	default:
		{
			return DefWindowProc( hWnd, msg, wParam, lParam );
		}
		break;
	}

	return 0;
}

void loadTexture()    
{
	int s;
	for (s = 0 ; s < header.numberOfSlices ; s ++)
	{
	int i,j;
	for( i = 0, j = 0 ; i < header.sliceHeight * header.sliceWidth*4; i += 4, j++)
	{
		pImage_RGBA[i]   = (unsigned char)(volume[s * header.sliceHeight * header.sliceWidth + j]*255);
		pImage_RGBA[i+1] = (unsigned char)(volume[s * header.sliceHeight * header.sliceWidth + j]*128);
		pImage_RGBA[i+2] = (unsigned char)(volume[s * header.sliceHeight * header.sliceWidth + j]*0);
		if (volume[s * header.sliceHeight * header.sliceWidth + j]<0.2f)
		{
			volume[s * header.sliceHeight * header.sliceWidth + j] = 0.0f;
		}
		pImage_RGBA[i+3] = (unsigned char)(volume[s * header.sliceHeight * header.sliceWidth + j]*20);
	}

	glBindTexture( GL_TEXTURE_2D, g_textureID[s] );
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_NEAREST);

	glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, header.sliceHeight, header.sliceWidth, 0,
		GL_RGBA, GL_UNSIGNED_BYTE, pImage_RGBA);
	}
}

void init( void )
{
	GLuint PixelFormat;

	PIXELFORMATDESCRIPTOR pfd;
	memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));

	pfd.nSize      = sizeof(PIXELFORMATDESCRIPTOR);
	pfd.nVersion   = 1;
	pfd.dwFlags    = PFD_DRAW_TO_WINDOW |PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
	pfd.iPixelType = PFD_TYPE_RGBA;
	pfd.cColorBits = 16;
	pfd.cDepthBits = 16;

	g_hDC = GetDC( g_hWnd );
	PixelFormat = ChoosePixelFormat( g_hDC, &pfd );
	SetPixelFormat( g_hDC, PixelFormat, &pfd);
	g_hRC = wglCreateContext( g_hDC );
	wglMakeCurrent( g_hDC, g_hRC );

	//--------------------------------------------------

	char *FileName = "C:/mandarine.r3d";
	fich.Open(FileName);
	fich.GetHeader(&header);
	volume = new float[header.numberOfSlices * header.sliceHeight * header.sliceWidth];
	fich.ReadVolume(volume);

	pImage_RGBA = new unsigned char[header.sliceHeight * header.sliceWidth * 4]; 

	float min = volume[0], max = volume[0];
	for (int k = 1; k < header.numberOfSlices * header.sliceHeight * header.sliceWidth; k++)
	{
		if (min > volume[k])
			min = volume[k];
		else if (max < volume[k])
			max = volume[k];
	}
	for (int k = 0; k < header.numberOfSlices * header.sliceHeight * header.sliceWidth; k++)
		volume[k] = (volume[k] - min) / (max - min);
	//--------------------------------------------------

	g_textureID = new GLuint[header.numberOfSlices];
	glGenTextures( header.numberOfSlices, g_textureID );

	glClearColor( 1.0f, 1.0f, 1.0f, 1.0f );
	glEnable(GL_TEXTURE_2D);

	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
	gluPerspective( 45.0f, 640.0f / 480.0f, 0.1f, 100.0f);

	int i,j;
	g_quadVertices = new Vertex[4*header.numberOfSlices];

	for (i = 0 , j = 0 ; j < header.numberOfSlices ; i += 4, j++)
	{
		g_quadVertices[i].tu = 0.0f;
		g_quadVertices[i].tv = 0.0f;
		g_quadVertices[i].x = -1.0f;
		g_quadVertices[i].y = -1.0f;
		g_quadVertices[i].z = (float)(-header.numberOfSlices/2 + j)/120.0f;

		g_quadVertices[i+1].tu = 1.0f;
		g_quadVertices[i+1].tv = 0.0f;
		g_quadVertices[i+1].x = 1.0f;
		g_quadVertices[i+1].y = -1.0f;
		g_quadVertices[i+1].z = (float)(-header.numberOfSlices/2 + j)/120.0f;

		g_quadVertices[i+2].tu = 1.0f;
		g_quadVertices[i+2].tv = 1.0f;
		g_quadVertices[i+2].x = 1.0f;
		g_quadVertices[i+2].y = 1.0f;
		g_quadVertices[i+2].z = (float)(-header.numberOfSlices/2 + j)/120.0f;

		g_quadVertices[i+3].tu = 0.0f;
		g_quadVertices[i+3].tv = 1.0f;
		g_quadVertices[i+3].x = -1.0f;
		g_quadVertices[i+3].y = 1.0f;
		g_quadVertices[i+3].z = (float)(-header.numberOfSlices/2 + j)/120.0f;
	}
	glInterleavedArrays( GL_T2F_V3F, 0, g_quadVertices );
	loadTexture();
}

void shutDown( void )   
{
	glDeleteTextures( header.numberOfSlices, g_textureID );
	delete[] g_quadVertices;
	delete[] pImage_RGBA;

	if( g_hRC != NULL )
	{
		wglMakeCurrent( NULL, NULL );
		wglDeleteContext( g_hRC );
		g_hRC = NULL;
	}

	if( g_hDC != NULL )
	{
		ReleaseDC( g_hWnd, g_hDC );
		g_hDC = NULL;
	}
}

void render( void )
{
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
	glTranslatef( 0.0f, 0.0f, g_zoom );
	glRotatef( -g_fSpinY, 1.0f, 0.0f, 0.0f );
	glRotatef( -g_fSpinX, 0.0f, 1.0f, 0.0f );

	if( g_bBlendOutColorKey == true )
	{
		glEnable( GL_BLEND );
		glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
	}

	//glInterleavedArrays( GL_T2F_V3F, 0, g_quadVertices );
	int s;
	for (s = 0 ; s < header.numberOfSlices ; s ++)
	{
		glBindTexture( GL_TEXTURE_2D, g_textureID[s] );
		glDrawArrays( GL_QUADS, s*4, 4 );
	}

	glDisable(GL_BLEND);

	SwapBuffers( g_hDC );
}


Hello,

for volume rendering you can choose between two basic approaches.
raycasting (prefare gpu raycasting) and 3d texture slicing.

I suggest you start with 3d texture slicing to get results fast.
you need to draw a cube. To keep it easy, the cube has the
length of 1.

Then you take the view ray of your camera and create planes perpendicular to the view ray. these planes cut the the cube. Then the intersection points are your 3d texture coordinates.

You’ll get 3 up to 6 intersectionpoints for each plane. depends on how the plane cuts the cube. with these itnersection points
you draw a polygon. So the Intersectionpoints are your 3d texture coordinates and your plane coordinates.

In a shader you can do simple 1d transfer fucntion operations.

like

uniform sampler3D volume;
uniform sampler2D transfer1d;
some structure
struct v2p
{

}
float4 fragmentProg(v2p IN) : COLOR0
{
float4 sample = tex3D(volume, IN.texCoord.xyz);
sample = tex1D(transfer1d, sample.r);
return sample;
}

When I’m at home I can show you some basic code.

Look at the pictures.

The coil in copper color is rendred with 3d texture slicing,
and local illumination. there is some noise you can see.
it is used to hide sampling artifacts. then the other grey coil, and the engine block are rendered with raycasting without local illumination. at the engine i applied a 1d transfer function. you can see some transparent parts and red parts.



regards,
lobbel