OpenGL Development Cookbook chapter7部分翻译

让我们通过以下简单步骤开始我们的配方:

1.通过读取外部的体数据文件,并通过该加载数据集数据转换成一个OpenGL纹理。也使硬件的mipmap生成。通常情况下,从使用一个横截面中获得的体积数据文件存储密度影像学检查方法,如CT或MRI扫描。每个CT/ MRI扫描是一个二维切片。我们在Z方向上积累简单的2D纹理的数组获得3D纹理。密度存储不同的材料的类型,例如值在0到20之间的是空气。当我们有一个8位无符号数据集,我们把数据集存储到GLubyte类型的本地数组。如果我们有一个16位无符号的数据集我们可以将其存储到GLushort类型的本地数组。3D纹理的情况下,除了S和T的参数,我们有一个额外的参数R控制我们在3D纹理下的切片。

std::ifstream infile(volume_file.c_str(), std::ios_base::binary);
if(infile.good()) {
GLubyte* pData = new GLubyte[XDIM*YDIM*ZDIM];
infile.read(reinterpret_cast<char*>(pData),
XDIM*YDIM*ZDIM*sizeof(GLubyte));
infile.close();
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_3D, textureID);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S,
GL_CLAMP);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T,
GL_CLAMP);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R,
GL_CLAMP);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER,
GL_LINEAR);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 0);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 4);
glTexImage3D(GL_TEXTURE_3D,0,GL_RED,XDIM,YDIM,ZDIM,0,GL_RED,GL_
UNSIGNED_BYTE,pData);
glGenerateMipmap(GL_TEXTURE_3D);
return true;
} else {
return false;
}

3D纹理的过滤参数类似于我们之前看到的2D纹理参数。Mipmap是被用于细节功能的纹理的下采样版本的集合。如果观众距离被应用纹理物体非常远,它们帮助使用下采样纹理。这有助于改善应用程序的性能。我们必须指定最大数目的层级(GL_TEXTURE_MAX_LEVEL),即是给定纹理生成的最大mipmap数。另外,基本层(GL_TEXTURE_BASE_LEVEL)表示当对象最近时第一级所使用的mipmap。

glGenerateMipMap函数的通过在之前的层级过滤减少操作生成派生数组作用。所以让我们说我们有三个mipmap层级,我们的3D纹理在层级0有256*256*256的分辨率。在层级1mipmap,0级数据通过过滤减少到一半大小至128*128*128.对于第二层级mipmap,层级一被过滤并减少到64*64*64.最后,作为第三层级,层级2将被过滤并减少到32*32*32.

2.设置一个顶点数组对象和顶点缓冲区对象存储的几何代理切片。确保缓冲区对象使用被指定为GL_DYNAMIC_DRAW。初始glBufferData 调用分配GPU内存以获得最大切片数。vTextureSlices数组是全局定义,它存储由顶点纹理切片操作三角产生。glBufferData用0初始化,该数据将在运行时被动态的填充。

const int MAX_SLICES = 512;
glm::vec3 vTextureSlices[MAX_SLICES*12];
glGenVertexArrays(1, &volumeVAO);
glGenBuffers(1, &volumeVBO);
glBindVertexArray(volumeVAO);
glBindBuffer (GL_ARRAY_BUFFER, volumeVBO);
glBufferData (GL_ARRAY_BUFFER, sizeof(vTextureSlices), 0, GL_
DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,0,0);
glBindVertexArray(0);

3.通过找到一个垂直于观察方向的具有代理切片的单位立方体交叉点实现体的切片。这是SliceVolume函数的功能。

因为我们的数据在所有三个轴具有相等大小即256*256*256.我们使用单位立方体。如果我们有不等尺寸的数据集我们可以适当的缩放单位立方体。

//determine max and min distances
glm::vec3 vecStart[12];
glm::vec3 vecDir[12];
float
float float float float lambda[12]; lambda_inc[12]; denom = 0; plane_dist = min_dist; plane_dist_inc = (max_dist-min_dist)/float(num_slices); //determine vecStart and vecDir values glm::vec3 intersection[6]; float dL[12]; for(int i=num_slices-1;i>=0;i--) { for(int e = 0; e < 12; e++) { dL[e] = lambda[e] + i*lambda_inc[e]; } if ((dL[0] >= 0.0) && (dL[0] < 1.0)) intersection[0] = vecStart[0] + dL[0]*vecDir[0]; { } //like wise for all intersection points int indices[]={0,1,2, 0,2,3, 0,3,4, 0,4,5}; for(int i=0;i<12;i++) vTextureSlices[count++]=intersection[indices[i]]; } //update buffer object glBindBuffer(GL_ARRAY_BUFFER, volumeVBO); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vTextureSlices), &(vTextureSlices[0].x));

4.在渲染函数,设置混合绑定体顶点数组对象,结合shader,然后调用glDrawArrays函数

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBindVertexArray(volumeVAO);
shader.Use();
glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_
ptr(MVP));
glDrawArrays(GL_TRIANGLES, 0, sizeof(vTextureSlices)/
sizeof(vTextureSlices[0]));
shader.UnUse();
glDisable(GL_BLEND);

它是如何工作的:

使用3D纹理切片体绘制接近由alpha混合纹理切片整体化的体绘制。第一步是加载并通过体数据生成3D纹理。装载体数据集之后,体切片由代理切片带出。这些体切片被定向在垂直于观察方向的方向。此外,我们需要找到代理多边形和单位立方体边界的交叉点。这由SliceVolume函数实现。需要注意的是切片只当视图转动时实现。

我们首先获得视线方向矢量(viewDir),它是模型试图矩阵的第三列。模型视图矩阵的第一列存放右向量,第二列存放上向量。我们现在详细介绍SliceVolume函数内部如何工作。我们通过计算在观看方向上8个单位顶点的最小最大距离找到当前观看方向最小最大顶点。这些距离通过每个单位立方体顶点与视线方向向量点积得到。

float max_dist = glm::dot(viewDir, vertexList[0]);
float min_dist = max_dist;
int max_index = 0;
int count = 0;
for(int i=1;i<8;i++) {
float dist = glm::dot(viewDir, vertexList[i]);
if(dist > max_dist) {
max_dist = dist;
max_index = i;
}
if(dist<min_dist)
min_dist = dist;
}
int max_dim = FindAbsMax(viewDir);
min_dist -= EPSILON;
max_dist += EPSILON;

从最近顶点走到最远顶点,从相机只有三个唯一路径。我们存储每个顶点的所有可能路径到一个边表,定义如下:

int edgeList[8][12]={{0,1,5,6, 4,8,11,9, 3,7,2,10 }, //v0 is front
{0,4,3,11, 1,2,6,7,5,9,8,10 }, //v1 is front
{1,5,0,8,2,3,7,4,6,10,9,11}, //v2 is front
{ 7,11,10,8, 2,6,1,9,3,0,4,5 }, // v3 is front
{ 8,5,9,1,11,10,7,6, 4,3,0,2 }, // v4 is front
{ 9,6,10,2, 8,11,4,7, 5,0,1,3 }, // v5 is front
{ 9,8,5,4,6,1,2,0,10,7,11,3}, // v6 is front
{ 10,9,6,5, 7,2,3,1,11,4,8,0 } // v7 is front

接下来,平面交叉口为单位立方体的12个边索引估计距离:

glm::vec3 vecStart[12];
glm::vec3 vecDir[12];
float lambda[12];
float lambda_inc[12];
float denom = 0;
float plane_dist = min_dist;
float plane_dist_inc = (max_dist-min_dist)/float(num_slices);
for(int i=0;i<12;i++) {
vecStart[i]=vertexList[edges[edgeList[max_index][i]][0]];
vecDir[i]=vertexList[edges[edgeList[max_index][i]][1]]-
vecStart[i];
denom = glm::dot(vecDir[i], viewDir);
if (1.0 + denom != 1.0) {
lambda_inc[i] = plane_dist_inc/denom;
lambda[i]=(plane_dist-glm::dot(vecStart[i],viewDir))/denom;
} else {
lambda[i]
= -1.0;
lambda_inc[i] = 0.0;
}
}

最后,通过在视线方向从后端到前端移动带出内插交点和单位立方体边。代理切片生成后,顶点缓冲对象用新的数据更新。

for(int i=num_slices-1;i>=0;i--) {
for(int e = 0; e < 12; e++) {
dL[e] = lambda[e] + i*lambda_inc[e];
}
if ((dL[0] >= 0.0) && (dL[0] < 1.0)) {
intersection[0] = vecStart[0] + dL[0]*vecDir[0];
} else if ((dL[1] >= 0.0) && (dL[1] < 1.0)) {
intersection[0] = vecStart[1] + dL[1]*vecDir[1];
} else if ((dL[3] >= 0.0) && (dL[3] < 1.0)) {
intersection[0] = vecStart[3] + dL[3]*vecDir[3];
} else continue;
if ((dL[2] >= 0.0) && (dL[2] < 1.0)){
intersection[1] = vecStart[2] + dL[2]*vecDir[2];
} else if ((dL[0] >= 0.0) && (dL[0] < 1.0)){
intersection[1] = vecStart[0] + dL[0]*vecDir[0];
} else if ((dL[1] >= 0.0) && (dL[1] < 1.0)){
intersection[1] = vecStart[1] + dL[1]*vecDir[1];
} else {intersection[1] = vecStart[3] + dL[3]*vecDir[3];
}
//similarly for others edges unitl intersection[5]
int indices[]={0,1,2, 0,2,3, 0,3,4, 0,4,5};
for(int i=0;i<12;i++)
vTextureSlices[count++]=intersection[indices[i]];
}
glBindBuffer(GL_ARRAY_BUFFER, volumeVBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vTextureSlices),
&(vTextureSlices[0].x));

在渲染函数,合适的shader被绑定。顶点shader通过对象空间顶点位置(vPosition)乘以混合模型视图投影(MVP)矩阵,计算出夹子空间位置。它还计算用于3D纹理坐标的体数据。因为我们渲染一个单位立方体,最小的顶点位置将是(-0.5,-0.5,-0.5)而最大顶点位置将是(0.5,0.5,0.5)。由于我们3D纹理查询需要从(0,0,0)到(1,1,1)的坐标。我们添加(0.5,0.5,0.5)到对象空间顶点位置来获得正确的3D纹理坐标。

smooth out vec3 vUV;
void main() {
gl_Position = MVP*vec4(vVertex.xyz,1);
vUV = vVertex + vec3(0.5);
}

然后片段shader使用3D纹理坐标进行体数据采样(其现在为3D纹理采用一个新的采样类型sampler3D)来显示密度。在创建3D纹理时,我们指定内部格式GL_RED(glTexImage3D函数的第三个参数)。因此,我们现在可以通过红色通道访问我们的密度。要获得灰色的shader,我们把绿色蓝色和alpha通道设为相同的值。

smooth in vec3 vUV;
uniform sampler3D volume;
void main(void) {
vFragColor = texture(volume, vUV).rrrr;
}

在以前的OpenGL版本,我们将体密度存储在一个特殊的内部格式GL_INTENSITY。这在OpenGL3.3核心特性已经过时了。所以现在我们必须使用GL_RED,GL_GREEN,GL_BLUE,或GL_ALPHA内部格式。

 

posted on 2015-01-30 15:50  褐鹤  阅读(990)  评论(0编辑  收藏  举报

导航