使用 OpenGL 入门体绘制






4.98/5 (64投票s)
逐步解释使用 OpenGL 进行 3D 图像渲染
- 下载 VolumeRendering_vs2010_bin.zip - 128.6 KB
- 下载 head256x256x109.zip - 3.9 MB
- 源代码 - https://github.com/divineaugustine/VolumeRendering_OGL
引言
我曾经是一名 Win32 应用程序开发人员,有一天,我被要求参与一个体渲染项目。我开始学习 OpenGL,但体渲染的学习非常困难。原因是对于体渲染,你有很多理论可以阅读,但没有与工作代码相关的解释,说明为什么会这样。本文旨在填补这一空白。
本文将逐步解释体渲染技术的基本概念。体渲染技术有不同的类型,如光线投射和基于纹理的渲染。本文演示了基于纹理的渲染。它从 2D 纹理方法开始,解释了使用它时存在的问题,并以 3D 纹理技术结束。尽管我使用 OpenGL 进行解释,但由于概念相同,可以很容易地在 DirectX 中创建它。
体渲染项目中有许多技术(如深度混合、MPR、LUT、传输函数、裁剪、使用着色器渲染等)。事实上,这些技术使体渲染项目完整。为了保持本文的简洁,我计划在另一篇文章中包含这些内容。
背景
本文需要 OpenGL 的基本知识,因为我不会深入解释每个 OpenGL API。文章附带的数据没有任何头文件,这使得处理起来更容易。维度(256x256x109 8 位)信息在应用程序中是硬编码的。我们可以从这里获取更多测试数据。但必须将其转换为原始数据。
示例应用程序使用 3D 纹理。因此,要运行此应用程序,计算机应支持 3D 纹理。这意味着 OpenGL 版本 1.2 或更高。
我们可以从这里获取 glew 库。本文中的 GIF 图像是使用此应用程序创建的。
使用应用程序
应用程序启动时会显示一个文件打开对话框。我们必须选择示例数据。如果按下鼠标左键,我们可以使用鼠标移动来旋转图像。
Using the Code
随附的源代码将只包含基于 3D 纹理的方法。
CRawDataProcessor
- 负责从文件中读取数据并将其转换为纹理CTranformationMgr
- 处理变换并将其保存在矩阵中CRendererHelper
- 执行 OpenGL 初始化和体渲染
什么是原始数据?
原始数据不过是连续的 2D 帧。下面是打开附加的原始数据后获得的切片的快照。这些 2D 帧将来自 Z 轴的不同位置。
设置 OpenGL
除了初始化 OpenGL 的常规步骤外,我们没有在这里做任何特殊的事情。我正在从 Dialog::OnCreate
调用 Initialize
函数进行初始化。Resize
函数处理正交投影的设置。由于 dialog::OnSize
将在启动和调整大小时调用,OnSize
是我们可以设置正交的地方。Render()
将从 OneraseBackground()
调用以绘制场景。当我们转向 3D 纹理时,需要 Glew.h 和 glew32.lib(以及支持的显卡)。这是因为 Windows 附带的 OpenGL 版本不支持高于 1.1 版本的规范。
如我所说,我们使用具有以下值的正交投影
- 左 = -1
- 右 = +1
- 上 = +1
- 下 = -1
- 近 = -1
- 远 = +1
上述值在 Resize
函数中可能会略有变化,因为我们保持了纵横比。检查调整大小的代码。但是,我们不必在渲染代码中考虑纵横比。
bool CRendererHelper::Initialize( HDC hContext_i )
{
//Setting up the dialog to support the OpenGL.
PIXELFORMATDESCRIPTOR stPixelFormatDescriptor;
memset( &stPixelFormatDescriptor, 0, sizeof( PIXELFORMATDESCRIPTOR ));
stPixelFormatDescriptor.nSize = sizeof( PIXELFORMATDESCRIPTOR );
stPixelFormatDescriptor.nVersion = 1;
stPixelFormatDescriptor.dwFlags =
PFD_DOUBLEBUFFER | PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW ;
stPixelFormatDescriptor.iPixelType = PFD_TYPE_RGBA;
stPixelFormatDescriptor.cColorBits = 24;
stPixelFormatDescriptor.cDepthBits = 32;
stPixelFormatDescriptor.cStencilBits = 8;
stPixelFormatDescriptor.iLayerType = PFD_MAIN_PLANE ;
int nPixelFormat = ChoosePixelFormat( hContext_i,
&stPixelFormatDescriptor ); //Collect the pixel format.
if( nPixelFormat == 0 )
{
AfxMessageBox( _T( "Error while Choosing Pixel format" ));
return false;
}
//Set the pixel format to the current dialog.
if( !SetPixelFormat( hContext_i, nPixelFormat, &stPixelFormatDescriptor ))
{
AfxMessageBox( _T( "Error while setting pixel format" ));
return false;
}
//Create a device context.
m_hglContext = wglCreateContext( hContext_i );
if( !m_hglContext )
{
AfxMessageBox( _T( "Rendering Context Creation Failed" ));
return false;
}
//Make the created device context as the current device context.
BOOL bResult = wglMakeCurrent( hContext_i, m_hglContext );
if( !bResult )
{
AfxMessageBox( _T( "wglMakeCurrent Failed" ));
return false;
}
glClearColor( 0.0f,0.0f, 0.0f, 0.0f );
glewInit(); // For 3D texture support
if(GL_TRUE != glewGetExtension("GL_EXT_texture3D"))
{
AfxMessageBox( _T( "3D texture is not supported !" ));
return false;
}
return true;
}
void CRendererHelper::Resize( int nWidth_i, int nHeight_i )
{
//Find the aspect ratio of the window.
GLdouble AspectRatio = ( GLdouble )(nWidth_i) / ( GLdouble )(nHeight_i );
//glViewport( 0, 0, cx , cy );
glViewport( 0, 0, nWidth_i, nHeight_i );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
//Set the orthographic projection.
if( nWidth_i <= nHeight_i )
{
glOrtho( -dOrthoSize, dOrthoSize, -( dOrthoSize / AspectRatio ) ,
dOrthoSize / AspectRatio, 2.0f*-dOrthoSize, 2.0f*dOrthoSize );
}
else
{
glOrtho( -dOrthoSize * AspectRatio, dOrthoSize * AspectRatio,
-dOrthoSize, dOrthoSize, 2.0f*-dOrthoSize, 2.0f*dOrthoSize );
}
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
}
基于 2D 纹理的方法
什么是纹理?
对于不知道纹理是什么的人,可以这样想,纹理是一个像 BITMAP
一样的结构。并且提供了一些函数来操作该结构。
使用纹理的主要步骤是
- 使用
glGenTextures
生成纹理。 - 绑定纹理并设置纹理的放大和缩小行为,以及超出范围条件(纹理坐标超出 0-1)的行为。
- 将数据加载到纹理中。
- 绘制顶点时,指定要映射的纹理坐标。
一旦数据加载到纹理中,实际维度就不再需要了,因为纹理坐标始终由 0-1 指定。无论纹理的大小如何,它都是 1。优点是我们只需要映射坐标,OpenGL 将处理所有所需的缩放。
glTexImage2D
是用于将数据加载到二维纹理的函数。函数的最后一个参数是保存数据的缓冲区。下面是 glTexImage2D
时发生的情况。
我正在创建 109 个(附加样本数据的 2D 切片数)2D 纹理来存储这些帧。这是读取文件和创建纹理的代码。体图像不过是在 Z 方向堆叠的 2D 帧。所以让我们这样排列这些 2D 纹理:
InitTextures2D()
创建与切片数一样多的纹理。并用每个切片加载每个纹理。Draw()
将纹理排列在 z 轴上。
bool InitTextures2D( LPCTSTR lpFileName_i )
{
CFile Medfile;
if( !Medfile.Open(lpFileName_i ,CFile::modeRead ))
{
AfxMessageBox( _T( "Failed to read the raw data" ));
return false;
}
// File has only image data. The dimension of the data should be known.
m_uImageCount = 109;
m_uImageWidth = 256;
m_uImageHeight = 256;
// Holds the texture IDs.
m_puTextureIDs = new int[m_uImageCount] ;
// Holds the luminance buffer
char* chBuffer = new char[ 256 * 256 ];
glGenTextures(m_uImageCount,(GLuint*)m_puTextureIDs );
// Read each frames and construct the texture
for( int nIndx = 0; nIndx < m_uImageCount; ++nIndx )
{
// Read the frame
Medfile.Read(chBuffer, m_uImageWidth*m_uImageHeight);
// Set the properties of the texture.
glBindTexture( GL_TEXTURE_2D, m_puTextureIDs[nIndx] );
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_uImageWidth, m_uImageHeight , 0,
GL_LUMINANCE, GL_UNSIGNED_BYTE,(GLvoid *) chBuffer);
glBindTexture( GL_TEXTURE_2D, 0 );
}
// glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
delete[] chBuffer;
return true;
}
void Render()
{
float fFrameCount = m_uImageCount;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glEnable(GL_DEPTH_TEST);glMatrixMode(GL_MODELVIEW):
glLoadIdentity();
glEnable(GL_TEXTURE_2D);
for(int nIndx=0; nIndx <m_uImageCount;++nIndx)
{
glBindTexture(GL_TEXTURE_2D,m_puTextureIds[nIndx]);
glBegin(GL_QUADS);
MAP_2DTEXT(nIndx);
glEnd();
glBindTexture(GL_TEXTURE_2D,0);
}
}
MAP_2DTEXT 宏做什么?
#define MAP_2DTEXT( TexIndex ) \
glTexCoord2f(0.0f, 0.0f); \
glVertex3f(-dViewPortSize,-dViewPortSize,(TexIndex *2*dViewPortSize/fFrameCount)-1.0f);\
glTexCoord2f(1.0f, 0.0f); \
glVertex3f(dViewPortSize,-dViewPortSize,(TexIndex *2*dViewPortSize/fFrameCount)-1.0f);\
glTexCoord2f(1.0f, 1.0f); \
glVertex3f(dViewPortSize,dViewPortSize,(TexIndex *2*dViewPortSize/fFrameCount)-1.0f);\
glTexCoord2f(0.0f, 1.0f); \
glVertex3f(-dViewPortSize,dViewPortSize,(TexIndex *2*dViewPortSize/fFrameCount)-1.0f);
上面的代码做了什么?它只是指定纹理坐标和四边形的相应顶点。纹理坐标 (0,0) 映射到顶点 (-1,-1),纹理坐标 (1,1) 映射到顶点 (1,1)。(0,1) 映射到 (-1,1),(1,0) 映射到 (1,-1)。这对于在 z 轴上绘制的每个四边形都重复。在 for
循环中,我们每次绑定的纹理都不同。由于 for
循环是从 0-109,顶点中的 z 位置需要进行一些转换,使其范围从 (-1) 到 (+1)。
我们看到什么了吗?是的,那是第一个切片。
这是因为;我们开始在 Z 轴上从 -1 到 +1 排列帧,并且由于深度测试属性,我们看到的是靠近我们的东西(参见正交投影)。
应用 Alpha
下一步是去除帧中的黑色,不是吗?在 OpenGL 中,有一个称为 Alpha 测试的功能。这意味着我们可以指定一个 alpha 值和 alpha 标准。
glEnable( GL_ALPHA_TEST );
glAlphaFunc( GL_GREATER, 0.05f );
这意味着 OpenGL 只会在像素的 alpha 值大于 0.05f 时才绘制该像素。但是我们的数据没有任何 alpha 值。让我们重新访问纹理创建并添加 alpha 值。
for( int nIndx = 0; nIndx < m_uImageCount; ++nIndx )
{
// Read the frame
Medfile.Read(chBuffer, m_uImageWidth*m_uImageHeight);
// Set the properties of the texture.
glBindTexture( GL_TEXTURE_2D, m_puTextureIDs[nIndx] );
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// Convert the data to RGBA data.
// Here we are simply putting the same value to R, G, B and A channels.
// Usually for raw data, the alpha value will
// be constructed by a threshold value given by the user
for( int nIndx = 0; nIndx < m_uImageWidth*m_uImageHeight; ++nIndx )
{
chRGBABuffer[nIndx*4] = chBuffer[nIndx];
chRGBABuffer[nIndx*4+1] = chBuffer[nIndx];
chRGBABuffer[nIndx*4+2] = chBuffer[nIndx];
chRGBABuffer[nIndx*4+3] = 255;
if( chBuffer[nIndx] < 20 )
{
chRGBABuffer[nIndx*4+3] = 0;
}
}
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_uImageWidth, m_uImageHeight , 0,
GL_RGBA, GL_UNSIGNED_BYTE,(GLvoid *) chRGBABuffer );
glBindTexture( GL_TEXTURE_2D, 0 );
}
void Render()
{
float fFrameCount = m_uImageCount;
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glEnable( GL_ALPHA_TEST );
glAlphaFunc( GL_GREATER, 0.5f );
glEnable( GL_DEPTH_TEST );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
glEnable(GL_TEXTURE_2D);
for ( int nIndx = 0; nIndx < m_uImageCount; nIndx++ )
{
glBindTexture( GL_TEXTURE_2D, m_puTextureIDs[nIndx]);
glBegin(GL_QUADS);
MAP_2DTEXT( nIndx );
glEnd();
glBindTexture( GL_TEXTURE_2D, 0 );
}
}
我们可以检查像素值并将其 alpha 值设置为 0(完全透明),对于所有其他像素值,可以将其设置为 255。此临时缓冲区提供给纹理。检查纹理的内部属性,glTexImage2D
中缓冲区的类型已更改。再次渲染。
如果我们有不同级别的 alpha,那么我们可以使用 glAlphaFunc
给出不同的值作为 alpha 标准。因此,为此,我们只是将 alpha 数据与此处的光度数据相同。这意味着亮度值为 0 的像素的 alpha 为 0,亮度值为 255 的像素的 alpha 为 255,介于两者之间的像素的 alpha 介于两者之间。
现在,如果我们尝试增加 glAlphaFunc
中第二个参数的值,我们可以看到类似这样的情况
应用混合
尽管我们已经设法获得了数据的概览,但它仍然看起来不佳。当我们在现实世界中观察一个物体时,光线会根据其透明度穿过物体,我们将得到直到那里的颜色混合。我们如何实现这一点?还记得 OpenGL 中的混合吗?是的,我们可以启用混合并绘制每一帧。
记住移除深度测试。
void CRendererHelper::Render()
{
float fFrameCount = m_uImageCount;
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glEnable(GL_BLEND);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
glRotated( mfRotation, 0, 1.0,0 );
glEnable(GL_TEXTURE_2D);
for ( int nIndx = 0; nIndx < m_uImageCount; nIndx++ )
{
glBindTexture( GL_TEXTURE_2D, m_puTextureIDs[nIndx]);
glBegin(GL_QUADS);
MAP_2DTEXT( nIndx );
glEnd();
glBindTexture( GL_TEXTURE_2D, 0 );
}
}
现在这看起来很不错,不是吗?
现在让我们对图像进行一些变换
使用 OpenGL 进行旋转很简单。使用 glRotate
。
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
glRotated( mfRotation, 0, 1.0,0 );
你注意到这里旋转时有一些问题吗?
旋转在 90 度后不正确
180 度图像看起来与没有任何旋转的图像的水平翻转图像完全一样。如果只绘制 2D 表面,这没问题,但对于 3D 数据,这是错误的。
为什么会有这种行为? 我们正在将纹理映射到四边形,并且旋转应用于四边形。我们使用混合来查看数据,并且没有深度测试。
让我们考虑一个没有旋转的情况。我们按顺序绘制 109 个四边形。在 z 轴 -1 处绘制的四边形映射 0texture
。这首先绘制在帧缓冲区上。然后在此之上,绘制索引为 1 的纹理,依此类推。由于没有启用深度测试,所以绘制的任何内容都会混合。
考虑 180 度旋转。
绘制和映射代码是相同的。但我们正在应用旋转。现在,当第一个四边形在 z 轴 -1 处绘制时,旋转被应用,它会旋转并定位在 +1 轴上,并且会水平翻转。索引为 1 的纹理将转到轴翻转的积极对应部分(想象一下从纸的另一面看图像)。像这样,所有纹理都会翻转并混合,导致图像水平翻转。
当它到达 90 度时屏幕变为空白
当它旋转到 90 度时,图像逐渐失去细节并在 90 度时变为空白。270 度也一样。请看图片。
为什么会发生这种情况?
当我们旋转四边形时,每当旋转时,我们都没有得到足够的样本进行混合。当它达到 90 度时,我们绘制的所有四边形都与我们的眼睛平行。换句话说,我们没有在 Y-Z 平面中绘制任何东西。因此,当 X-Y 平面旋转 90 度时,由于 OpenGL 不绘制四边形的边缘,我们看不到任何东西。
要获得 180 度旋转的实际图像,我们需要首先绘制另一端的纹理。
我们如何解决这个问题?
纹理旋转
OpenGL 支持一个称为纹理矩阵的矩阵。我们可以在纹理矩阵中应用变换,它会在纹理映射期间受到影响。但对于 2D 纹理来说,这毫无用处。
转向 3D 纹理
3D 纹理具有 z 轴的优势。就像宽度和高度在 2D 纹理中被归一化为 1 一样,切片数量或 z 轴在 3D 纹理中也被归一化为 1。这将如何解决我们 2D 纹理的变换问题?
让我们更改四边形映射代码和变换。
#define MAP_3DTEXT( TexIndex ) \
glTexCoord3f(0.0f, 0.0f, ((float)TexIndex+1.0f)/2.0f); \
glVertex3f(-dViewPortSize,-dViewPortSize,TexIndex);\
glTexCoord3f(1.0f, 0.0f, ((float)TexIndex+1.0f)/2.0f); \
glVertex3f(dViewPortSize,-dViewPortSize,TexIndex);\
glTexCoord3f(1.0f, 1.0f, ((float)TexIndex+1.0f)/2.0f); \
glVertex3f(dViewPortSize,dViewPortSize,TexIndex);\
glTexCoord3f(0.0f, 1.0f, ((float)TexIndex+1.0f)/2.0f); \
glVertex3f(-dViewPortSize,dViewPortSize,TexIndex);
bool CRendererHelper::InitTextures3D( LPCTSTR lpFileName_i )
{
CFile Medfile;
if( !Medfile.Open(lpFileName_i ,CFile::modeRead ))
{
AfxMessageBox( _T( "Failed to read the raw data" ));
return false;
}
// File has only image data. The dimension of the data should be known.
m_uImageCount = 109;
m_uImageWidth = 256;
m_uImageHeight = 256;
// Holds the texture IDs.
m_puTextureIDs = new int[m_uImageCount] ;
// Holds the luminance buffer
char* chBuffer = new char[ m_uImageWidth*m_uImageHeight *m_uImageCou ];
// Holds the RGBA buffer
char* chRGBABuffer = new char[ m_uImageWidth*m_uImageHeight*m_uImageHeight*4 ];
glGenTextures(1,(GLuint*)&mu3DTex );
Medfile.Read(chBuffer, m_uImageWidth*m_uImageHeight*m_uImageCount);
glBindTexture( GL_TEXTURE_3D, mu3DTex );
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// Convert the data to RGBA data.
// Here we are simply putting the same value to R, G, B and A channels.
// Usually for raw data, the alpha value
// will be constructed by a threshold value given by the user
for( int nIndx = 0; nIndx < m_uImageWidth*m_uImageHeight*m_uImageCount; ++nIndx )
{
chRGBABuffer[nIndx*4] = chBuffer[nIndx];
chRGBABuffer[nIndx*4+1] = chBuffer[nIndx];
chRGBABuffer[nIndx*4+2] = chBuffer[nIndx];
chRGBABuffer[nIndx*4+3] = chBuffer[nIndx];
}
glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA,
m_uImageWidth, m_uImageHeight , m_uImageCount, 0,
GL_RGBA, GL_UNSIGNED_BYTE,(GLvoid *) chRGBABuffer );
glBindTexture( GL_TEXTURE_3D, 0 );
delete[] chBuffer;
delete[] chRGBABuffer;
return true;
}
void CRendererHelper::Render()
{
float fFrameCount = m_uImageCount;
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glEnable( GL_ALPHA_TEST );
glAlphaFunc( GL_GREATER, 0.03f );
glEnable(GL_BLEND);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glMatrixMode( GL_TEXTURE );
glLoadIdentity();
// Translate and make 0.5f as the center
// (texture co ordinate is from 0 to 1.
// so center of rotation has to be 0.5f)
glTranslatef( 0.5f, 0.5f, 0.5f );
glRotated( mfRotation, 0, 1.0,0 );
glTranslatef( -0.5f,-0.5f, -0.5f );
glEnable(GL_TEXTURE_3D);
glBindTexture( GL_TEXTURE_3D, mu3DTex );
for ( float fIndx = -1.0f; fIndx <= 1.0f; fIndx+=0.003f )
{
glBegin(GL_QUADS);
MAP_3DTEXT( fIndx );
glEnd();
}
}
现在旋转应用于纹理矩阵。四边形与 3D 纹理的相应 z 轴位置进行映射。因此,当我们应用旋转时,四边形永远不会变换。四边形始终在 XY 平面中绘制。它映射的纹理的 2D 平面旋转。因此,对于 180 度旋转,在 -1 Z 轴绘制的四边形映射纹理 z 轴中的纹理坐标 1。由于 109 个平面是平行绘制的,因此在任何方向上始终有 109 个像素要混合。无论切片数量如何,我们都可以绘制更多平面并映射 3D 纹理的相应 z 位置,因为纹理具有插值属性。
当我们使用纹理旋转时,我们应该使用 GL_CLAMP_TO_BORDER
的纹理属性以获得正确的图像。因为当纹理坐标旋转时,纹理坐标甚至可能超出 0-1。当坐标超出该范围时,我们不希望发出数据。
我们有 256 个像素在 x 轴上,256 个在 y 轴上,109 个在 z 轴上。看看当我们将图像沿 y 方向旋转 90 度时会发生什么。现在显示 z 侧作为 x 轴。我们将所有轴上的值从 -1 映射到 +1。所以现在 109 个像素被映射到轴上的 -1 到 +1,我们可以看到那一边看起来比另一边大。我们应该缩放它以获得正确的大小。通常,对于实际数据,我们会在 x、y 和 z 中获得每毫米像素值,以便我们可以相应地缩放。但这里只是原始数据。所以我使用维度本身进行缩放。在这里,我将宽度映射到 -1 - +1,所有其他边都根据宽度进行缩放。
此外,我在 y 轴上应用了负比例以避免由于 gltexImage3D
导致的翻转(参见 gltexImage2D
数据传输)。这种翻转也可以通过颠倒纹理映射来实现。
glMatrixMode( GL_TEXTURE );
glLoadIdentity();
// Translate and make 0.5f as the center
// (texture co ordinate is from 0 to 1. so center of rotation has to be 0.5f)
glTranslatef( 0.5f, 0.5f, 0.5f );
// A scaling applied to normalize the axis
// (Usually the number of slices will be less so if this is not -
// normalized then the z axis will look bulky)
// Flipping of the y axis is done by giving a negative value in y axis.
// This can be achieved either by changing the y co ordinates in -
// texture mapping or by negative scaling of y axis
glScaled( (float)m_pRawDataProc->GetWidth()/(float)m_pRawDataProc->GetWidth(),
-1.0f*(float)m_pRawDataProc->GetWidth()/(float)(float)m_pRawDataProc->GetHeight(),
(float)m_pRawDataProc->GetWidth()/(float)m_pRawDataProc->GetDepth());
// Apply the user provided transformations
glMultMatrixd( m_pTransformMgr->GetMatrix());
glTranslatef( -0.5f,-0.5f, -0.5f );
关注点
如果你想处理不同维度和类型(LUMINANCE 16 等)的数据,那么必须在 CVolumeRenderingDlg::OnInitDialog
和 CRawDataProcessor::ReadFile
中进行更改。如果维度不是 2 的幂,那么在执行 glTexImage3D
之前必须校正像素对齐。
历史
- 2012 年 10 月 26 日 - 初始版本
- 2012 年 10 月 31 日 - 更新文章
- 2012 年 6 月 30 日 - 修复了代码中的这个错误。感谢 Englishclive 指出。