65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 OpenGL 入门体绘制

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (64投票s)

2012年10月26日

CPOL

11分钟阅读

viewsIcon

297112

downloadIcon

17851

逐步解释使用 OpenGL 进行 3D 图像渲染

引言

我曾经是一名 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.hglew32.lib(以及支持的显卡)。这是因为 Windows 附带的 OpenGL 版本不支持高于 1.1 版本的规范。

如我所说,我们使用具有以下值的正交投影

  • 左 = -1
  • 右 = +1
  • 上 = +1
  • 下 = -1
  • 近 = -1
  • 远 = +1

上述值在 Resize 函数中可能会略有变化,因为我们保持了纵横比。检查调整大小的代码。但是,我们不必在渲染代码中考虑纵横比。

Ortho

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 一样的结构。并且提供了一些函数来操作该结构。

使用纹理的主要步骤是

  1. 使用 glGenTextures 生成纹理。
  2. 绑定纹理并设置纹理的放大和缩小行为,以及超出范围条件(纹理坐标超出 0-1)的行为。
  3. 将数据加载到纹理中。
  4. 绘制顶点时,指定要映射的纹理坐标。

一旦数据加载到纹理中,实际维度就不再需要了,因为纹理坐标始终由 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::OnInitDialogCRawDataProcessor::ReadFile 中进行更改。如果维度不是 2 的幂,那么在执行 glTexImage3D 之前必须校正像素对齐。

历史

  • 2012 年 10 月 26 日 - 初始版本
  • 2012 年 10 月 31 日 - 更新文章
  • 2012 年 6 月 30 日 - 修复了代码中的这个错误。感谢 Englishclive 指出。
© . All rights reserved.