本文描述了一个创建三维标量场等值面多边形曲面表示的算法。这类问题的一个常见名称是所谓的“移动立方体”算法。它结合了简单和高速,因为它几乎完全用于查找表。
这种技术有很多应用,两个非常常见的是:
从医学体积数据集重建表面。例如,MRI扫描在常规3d网格的顶点处产生一个3d体积的样本。
创建数学标量场的三维轮廓。在这种情况下,函数是已知的,但在一个常规的3D网格的顶点采样。
1.解决方案
其基本问题是通过在矩形三维网格上采样的标量场形成一个面逼近等值面。给定一个由顶点和每个顶点的标量值定义的网格单元,有必要创建通过该网格单元最能代表等值面的平面facet。等值面可能不会穿过网格单元格,它可能切断任何一个顶点,或者它可能通过许多更复杂的方式中的任何一种。每一种可能性都将以等值面以上或以下值的顶点数来表征。如果一个顶点在等值面上,而另一个相邻顶点在等值面上,那么我们就知道等值面上切割这两个顶点之间的边。它切割边缘的位置将被线性插值,两个顶点之间的长度之比将与等值面值与网格单元格顶点处值之比相同。
算法中使用的顶点和边的索引约定如下所示
例如,如果顶点3的值低于等值面值,而所有其他顶点的值都高于等值面值,那么我们将创建一个三角形面,它穿过边2、3和11。三角形面的顶点的确切位置分别取决于等值面值与顶点3-2、3-0、3-7上的值的关系。
使算法“困难”的是大量的可能组合(256个),以及需要为每个解决方案导出一致的facet组合,以便相邻网格单元的facet正确地连接在一起。
算法的第一部分使用一个表(edgeTable),它将等值面下的顶点映射到相交的边缘。一个8位的索引是由每个位对应一个顶点形成的。
cubeindex = 0;
if (grid.val[0] < isolevel) cubeindex |= 1;
if (grid.val[1] < isolevel) cubeindex |= 2;
if (grid.val[2] < isolevel) cubeindex |= 4;
if (grid.val[3] < isolevel) cubeindex |= 8;
if (grid.val[4] < isolevel) cubeindex |= 16;
if (grid.val[5] < isolevel) cubeindex |= 32;
if (grid.val[6] < isolevel) cubeindex |= 64;
if (grid.val[7] < isolevel) cubeindex |= 128;
查找边缘表返回一个12位的数字,每一位对应一条边,0表示该边没有被等值面切割,1表示该边被等值面切割。如果没有任何边被切割,则表返回0,当cubeindex为0(等值面以下的所有顶点)或0xff(等值面以上的所有顶点)时就会发生这种情况。
使用前面的例子,只有顶点3低于等值面,cubeindex将等于0000 1000或8。edgeTable[8] = 1000 0000 1100。这意味着边2,3和11与等值面相交。
交点现在由线性插值计算。如果P1和P2是切边的两个顶点,V1和V2是每个顶点的标量值,那么交点P是
P = P1 +(等值- V1) (P2 - P1) / (V2 - V1)
算法的最后一部分涉及到从等值面与网格单元的边缘相交的位置形成正确的facet。同样使用了一个表(triTable),这次使用相同的立方体索引,但允许查找顶点序列,因为在网格单元中表示等值面需要多少三角形面就需要多少三角形面。最多需要5个三角形面。
回到我们的例子,在前面的步骤中,我们计算了沿边2、3和11的交点。triTable中的第8个元素是
{3、11、2、1,1,1,1,1,1,1,1,1,1,1,1,1},
这是一个特别简单的示例,请确保facet组合对于表中的许多情况不是那么明显。
2.举例子
假设顶点0和3在等值面以下。Cubeindex将是0000 1001 == 9。进入egdeTable的第9项是905hex == 1001 0000 0101,这意味着边11、8、2和0被切割,所以我们计算出等面与这些边相交的顶点。
接下来,triitable中的9是0,11,2,8,11,0。这对应于两个三角形面片,一个在边0,11和2的交点之间。另一个在沿边8 11和0的交点之间。
3.网格分辨率
当对一个值已知或可以在空间中任意位置插值的字段进行多边形化时,一个非常理想的控制是采样网格的分辨率。这允许根据所需的平滑度和/或显示表面的处理能力生成等面的过程或精细近似。
4.源代码
基于OpenGL绘制(VS可直接运行)
#include "stdio.h"
#include "math.h"
//This program requires the OpenGL and GLUT libraries
// You can obtain them for free from http://www.opengl.org
#include "GL/glut.h"
struct GLvector
{
GLfloat fX;
GLfloat fY;
GLfloat fZ;
};
//These tables are used so that everything can be done in little loops that you can look at all at once
// rather than in pages and pages of unrolled code.
//a2fVertexOffset lists the positions, relative to vertex0, of each of the 8 vertices of a cube
static const GLfloat a2fVertexOffset[8][3] =
{
{0.0, 0.0, 0.0},{1.0, 0.0, 0.0},{1.0, 1.0, 0.0},{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0},{1.0, 0.0, 1.0},{1.0, 1.0, 1.0},{0.0, 1.0, 1.0}
};
//a2iEdgeConnection lists the index of the endpoint vertices for each of the 12 edges of the cube
static const GLint a2iEdgeConnection[12][2] =
{
{0,1}, {1,2}, {2,3}, {3,0},
{4,5}, {5,6}, {6,7}, {7,4},
{0,4}, {1,5}, {2,6}, {3,7}
};
//a2fEdgeDirection lists the direction vector (vertex1-vertex0) for each edge in the cube
static const GLfloat a2fEdgeDirection[12][3] =
{
{1.0, 0.0, 0.0},{0.0, 1.0, 0.0},{-1.0, 0.0, 0.0},{0.0, -1.0, 0.0},
{1.0, 0.0, 0.0},{0.0, 1.0, 0.0},{-1.0, 0.0, 0.0},{0.0, -1.0, 0.0},
{0.0, 0.0, 1.0},{0.0, 0.0, 1.0},{ 0.0, 0.0, 1.0},{0.0, 0.0, 1.0}
};
//a2iTetrahedronEdgeConnection lists the index of the endpoint vertices for each of the 6 edges of the tetrahedron
static const GLint a2iTetrahedronEdgeConnection[6][2] =
{
{0,1}, {1,2}, {2,0}, {0,3}, {1,3}, {2,3}
};
//a2iTetrahedronEdgeConnection lists the index of verticies from a cube
// that made up each of the six tetrahedrons within the cube
static const GLint a2iTetrahedronsInACube[6][4] =
{
{0,5,1,6},
{0,1,2,6},
{0,2,3,6},
{0,3,7,6},
{0,7,4,6},
{0,4,5,6},
};
static const GLfloat afAmbientWhite [] = {0.25, 0.25, 0.25, 1.00};
static const GLfloat afAmbientRed [] = {0.25, 0.00, 0.00, 1.00};
static const GLfloat afAmbientGreen [] = {0.00, 0.25, 0.00, 1.00};
static const GLfloat afAmbientBlue [] = {0.00, 0.00, 0.25, 1.00};
static const GLfloat afDiffuseWhite [] = {0.75, 0.75, 0.75, 1.00};
static const GLfloat afDiffuseRed [] = {0.75, 0.00, 0.00, 1.00};
static const GLfloat afDiffuseGreen [] = {0.00, 0.75, 0.00, 1.00};
static const GLfloat afDiffuseBlue [] = {0.00, 0.00, 0.75, 1.00};
static const GLfloat afSpecularWhite[] = {1.00, 1.00, 1.00, 1.00};
static const GLfloat afSpecularRed [] = {1.00, 0.25, 0.25, 1.00};
static const GLfloat afSpecularGreen[] = {0.25, 1.00, 0.25, 1.00};
static const GLfloat afSpecularBlue [] = {0.25, 0.25, 1.00, 1.00};
GLenum ePolygonMode = GL_LINE;
GLint iDataSetSize = 26;
GLfloat fStepSize = 1.0/iDataSetSize;
GLfloat fTargetValue = 48.0;
GLfloat fTime = 0.0;
GLvector sSourcePoint[3];
GLboolean bSpin = true;
GLboolean bMove = true;
GLboolean bLight = true;
void vIdle();
void vDrawScene();
void vResize(GLsizei, GLsizei);
void vKeyboard(unsigned char cKey, int iX, int iY);
void vSpecial(int iKey, int iX, int iY);
GLvoid vPrintHelp();
GLvoid vSetTime(GLfloat fTime);
GLfloat fSample1(GLfloat fX, GLfloat fY, GLfloat fZ);
GLfloat fSample2(GLfloat fX, GLfloat fY, GLfloat fZ);
GLfloat fSample3(GLfloat fX, GLfloat fY, GLfloat fZ);
GLfloat (*fSample)(GLfloat fX, GLfloat fY, GLfloat fZ) = fSample1;
GLvoid vMarchingCubes();
GLvoid vMarchCube1(GLfloat fX, GLfloat fY, GLfloat fZ, GLfloat fScale);
GLvoid vMarchCube2(GLfloat fX, GLfloat fY, GLfloat fZ, GLfloat fScale);
GLvoid (*vMarchCube)(GLfloat fX, GLfloat fY, GLfloat fZ, GLfloat fScale) = vMarchCube1;
void main(int argc, char **argv)
{
GLfloat afPropertiesAmbient [] = {0.50, 0.50, 0.50, 1.00};
GLfloat afPropertiesDiffuse [] = {0.75, 0.75, 0.75, 1.00};
GLfloat afPropertiesSpecular[] = {1.00, 1.00, 1.00, 1.00};
GLsizei iWidth = 640.0;
GLsizei iHeight = 480.0;
glutInit(&argc, argv);
glutInitWindowPosition( 0, 0);
glutInitWindowSize(iWidth, iHeight);
glutInitDisplayMode( GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE );
glutCreateWindow( "Marching Cubes" );
glutDisplayFunc( vDrawScene );
glutIdleFunc( vIdle );
glutReshapeFunc( vResize );
glutKeyboardFunc( vKeyboard );
glutSpecialFunc( vSpecial );
glClearColor( 0.0, 0.0, 0.0, 1.0 );
glClearDepth( 1.0 );
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glPolygonMode(GL_FRONT_AND_BACK, ePolygonMode);
glLightfv( GL_LIGHT0, GL_AMBIENT, afPropertiesAmbient);
glLightfv( GL_LIGHT0, GL_DIFFUSE, afPropertiesDiffuse);
glLightfv( GL_LIGHT0, GL_SPECULAR, afPropertiesSpecular);
glLightModelf(GL_LIGHT_MODEL_TWO_SIDE, 1.0);
glEnable( GL_LIGHT0 );
glMaterialfv(GL_BACK, GL_AMBIENT, afAmbientGreen);
glMaterialfv(GL_BACK, GL_DIFFUSE, afDiffuseGreen);
glMaterialfv(GL_FRONT, GL_AMBIENT, afAmbientBlue);
glMaterialfv(GL_FRONT, GL_DIFFUSE, afDiffuseBlue);
glMaterialfv(GL_FRONT, GL_SPECULAR, afSpecularWhite);
glMaterialf( GL_FRONT, GL_SHININESS, 25.0);
vResize(iWidth, iHeight);
vPrintHelp();
glutMainLoop();
}
GLvoid vPrintHelp()
{
printf("Marching Cubes Example by Cory Bloyd (dejaspaminacan@my-deja.com)nn");
printf("+/- increase/decrease sample densityn");
printf("PageUp/PageDown increase/decrease surface valuen");
printf("s change sample functionn");
printf("c toggle marching cubes / marching tetrahedronsn");
printf("w wireframe on/offn");
printf("l toggle lighting / color-by-normaln");
printf("Home spin scene on/offn");
printf("End source point animation on/offn");
}
void vResize( GLsizei iWidth, GLsizei iHeight )
{
GLfloat fAspect, fHalfWorldSize = (1.4142135623730950488016887242097/2);
glViewport( 0, 0, iWidth, iHeight );
glMatrixMode (GL_PROJECTION);
glLoadIdentity ();
if(iWidth <= iHeight)
{
fAspect = (GLfloat)iHeight / (GLfloat)iWidth;
glOrtho(-fHalfWorldSize, fHalfWorldSize, -fHalfWorldSize*fAspect,
fHalfWorldSize*fAspect, -10*fHalfWorldSize, 10*fHalfWorldSize);
}
else
{
fAspect = (GLfloat)iWidth / (GLfloat)iHeight;
glOrtho(-fHalfWorldSize*fAspect, fHalfWorldSize*fAspect, -fHalfWorldSize,
fHalfWorldSize, -10*fHalfWorldSize, 10*fHalfWorldSize);
}
glMatrixMode( GL_MODELVIEW );
}
void vKeyboard(unsigned char cKey, int iX, int iY)
{
switch(cKey)
{
case 'w' :
{
if(ePolygonMode == GL_LINE)
{
ePolygonMode = GL_FILL;
}
else
{
ePolygonMode = GL_LINE;
}
glPolygonMode(GL_FRONT_AND_BACK, ePolygonMode);
} break;
case '+' :
case '=' :
{
++iDataSetSize;
fStepSize = 1.0/iDataSetSize;
} break;
case '-' :
{
if(iDataSetSize > 1)
{
--iDataSetSize;
fStepSize = 1.0/iDataSetSize;
}
} break;
case 'c' :
{
if(vMarchCube == vMarchCube1)
{
vMarchCube = vMarchCube1;//Use Marching Tetrahedrons
}
else
{
vMarchCube = vMarchCube1;//Use Marching Cubes
}
} break;
case 's' :
{
if(fSample == fSample1)
{
fSample = fSample1;
}
else if(fSample == fSample2)
{
fSample = fSample1;
}
else
{
fSample = fSample1;
}
} break;
case 'l' :
{
if(bLight)
{
glDisable(GL_LIGHTING);//use vertex colors
}
else
{
glEnable(GL_LIGHTING);//use lit material color
}
bLight = !bLight;
};
}
}
void vSpecial(int iKey, int iX, int iY)
{
switch(iKey)
{
case GLUT_KEY_PAGE_UP :
{
if(fTargetValue < 1000.0)
{
fTargetValue *= 1.1;
}
} break;
case GLUT_KEY_PAGE_DOWN :
{
if(fTargetValue > 1.0)
{
fTargetValue /= 1.1;
}
} break;
case GLUT_KEY_HOME :
{
bSpin = !bSpin;
} break;
case GLUT_KEY_END :
{
bMove = !bMove;
} break;
}
}
void vIdle()
{
glutPostRedisplay();
}
void vDrawScene()
{
static GLfloat fPitch = 0.0;
static GLfloat fYaw = 0.0;
static GLfloat fTime = 0.0;
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glPushMatrix();
if(bSpin)
{
fPitch += 4.0;
fYaw += 2.5;
}
if(bMove)
{
fTime += 0.025;
}
vSetTime(fTime);
glTranslatef(0.0, 0.0, -1.0);
glRotatef( -fPitch, 1.0, 0.0, 0.0);
glRotatef( 0.0, 0.0, 1.0, 0.0);
glRotatef( fYaw, 0.0, 0.0, 1.0);
glPushAttrib(GL_LIGHTING_BIT);
glDisable(GL_LIGHTING);
glColor3f(1.0, 1.0, 1.0);
glutWireCube(1.0);
glPopAttrib();
glPushMatrix();
glTranslatef(-0.5, -0.5, -0.5);
glBegin(GL_TRIANGLES);
vMarchingCubes();
glEnd();
glPopMatrix();
glPopMatrix();
glutSwapBuffers();
}
//fGetOffset finds the approximate point of intersection of the surface
// between two points with the values fValue1 and fValue2
GLfloat fGetOffset(GLfloat fValue1, GLfloat fValue2, GLfloat fValueDesired)
{
GLdouble fDelta = fValue2 - fValue1;
if(fDelta == 0.0)
{
return 0.5;
}
return (fValueDesired - fValue1)/fDelta;
}
//vGetColor generates a color from a given position and normal of a point
GLvoid vGetColor(GLvector &rfColor, GLvector &rfPosition, GLvector &rfNormal)
{
GLfloat fX = rfNormal.fX;
GLfloat fY = rfNormal.fY;
GLfloat fZ = rfNormal.fZ;
rfColor.fX = (fX > 0.0 ? fX : 0.0) + (fY < 0.0 ? -0.5*fY : 0.0) + (fZ < 0.0 ? -0.5*fZ : 0.0);
rfColor.fY = (fY > 0.0 ? fY : 0.0) + (fZ < 0.0 ? -0.5*fZ : 0.0) + (fX < 0.0 ? -0.5*fX : 0.0);
rfColor.fZ = (fZ > 0.0 ? fZ : 0.0) + (fX < 0.0 ? -0.5*fX : 0.0) + (fY < 0.0 ? -0.5*fY : 0.0);
}
GLvoid vNormalizeVector(GLvector &rfVectorResult, GLvector &rfVectorSource)
{
GLfloat fOldLength;
GLfloat fScale;
fOldLength = sqrtf( (rfVectorSource.fX * rfVectorSource.fX) +
(rfVectorSource.fY * rfVectorSource.fY) +
(rfVectorSource.fZ * rfVectorSource.fZ) );
if(fOldLength == 0.0)
{
rfVectorResult.fX = rfVectorSource.fX;
rfVectorResult.fY = rfVectorSource.fY;
rfVectorResult.fZ = rfVectorSource.fZ;
}
else
{
fScale = 1.0/fOldLength;
rfVectorResult.fX = rfVectorSource.fX*fScale;
rfVectorResult.fY = rfVectorSource.fY*fScale;
rfVectorResult.fZ = rfVectorSource.fZ*fScale;
}
}
//Generate a sample data set. fSample1(), fSample2() and fSample3() define three scalar fields whose
// values vary by the X,Y and Z coordinates and by the fTime value set by vSetTime()
GLvoid vSetTime(GLfloat fNewTime)
{
GLfloat fOffset;
GLint iSourceNum;
for(iSourceNum = 0; iSourceNum < 3; iSourceNum++)
{
sSourcePoint[iSourceNum].fX = 0.5;
sSourcePoint[iSourceNum].fY = 0.5;
sSourcePoint[iSourceNum].fZ = 0.5;
}
fTime = fNewTime;
fOffset = 1.0 + sinf(fTime);
sSourcePoint[0].fX *= fOffset;
sSourcePoint[1].fY *= fOffset;
sSourcePoint[2].fZ *= fOffset;
}
//fSample1 finds the distance of (fX, fY, fZ) from three moving points
GLfloat fSample3(GLfloat fX, GLfloat fY, GLfloat fZ)
{
GLdouble fResult = 0.0;
GLdouble fDx, fDy, fDz;
fDx = fX - sSourcePoint[0].fX;
fDy = fY - sSourcePoint[0].fY;
fDz = fZ - sSourcePoint[0].fZ;
fResult += 0.5/(fDx*fDx + fDy*fDy + fDz*fDz);
fDx = fX - sSourcePoint[1].fX;
fDy = fY - sSourcePoint[1].fY;
fDz = fZ - sSourcePoint[1].fZ;
fResult += 1.0/(fDx*fDx + fDy*fDy + fDz*fDz);
fDx = fX - sSourcePoint[2].fX;
fDy = fY - sSourcePoint[2].fY;
fDz = fZ - sSourcePoint[2].fZ;
fResult += 1.5/(fDx*fDx + fDy*fDy + fDz*fDz);
return fResult;
}
//fSample2 finds the distance of (fX, fY, fZ) from three moving lines
GLfloat fSample2(GLfloat fX, GLfloat fY, GLfloat fZ)
{
GLdouble fResult = 0.0;
GLdouble fDx, fDy, fDz;
fDx = fX - sSourcePoint[0].fX;
fDy = fY - sSourcePoint[0].fY;
fResult += 0.5/(fDx*fDx + fDy*fDy);
fDx = fX - sSourcePoint[1].fX;
fDz = fZ - sSourcePoint[1].fZ;
fResult += 0.75/(fDx*fDx + fDz*fDz);
fDy = fY - sSourcePoint[2].fY;
fDz = fZ - sSourcePoint[2].fZ;
fResult += 1.0/(fDy*fDy + fDz*fDz);
return fResult;
}
//fSample2 defines a height field by plugging the distance from the center into the sin and cos functions
GLfloat fSample1(GLfloat fX, GLfloat fY, GLfloat fZ)
{
GLfloat fHeight = ((sqrt((0.5-fX)*(0.5-fX) + (0.5-fY)*(0.5-fY))) +48);
GLdouble fResult = (fHeight-fZ);
return fResult;
}
//vGetNormal() finds the gradient of the scalar field at a point
//This gradient can be used as a very accurate vertx normal for lighting calculations
GLvoid vGetNormal(GLvector &rfNormal, GLfloat fX, GLfloat fY, GLfloat fZ)
{
rfNormal.fX = fSample(fX-0.01, fY, fZ) - fSample(fX+0.01, fY, fZ);
rfNormal.fY = fSample(fX, fY-0.01, fZ) - fSample(fX, fY+0.01, fZ);
rfNormal.fZ = fSample(fX, fY, fZ-0.01) - fSample(fX, fY, fZ+0.01);
vNormalizeVector(rfNormal, rfNormal);
}
//vMarchCube1 performs the Marching Cubes algorithm on a single cube
GLvoid vMarchCube1(GLfloat fX, GLfloat fY, GLfloat fZ, GLfloat fScale)
{
extern GLint aiCubeEdgeFlags[256];
extern GLint a2iTriangleConnectionTable[256][16];
GLint iCorner, iVertex, iVertexTest, iEdge, iTriangle, iFlagIndex, iEdgeFlags;
GLfloat fOffset;
GLvector sColor;
GLfloat afCubeValue[8];
GLvector asEdgeVertex[12];
GLvector asEdgeNorm[12];
//Make a local copy of the values at the cube's corners
for(iVertex = 0; iVertex < 8; iVertex++)
{
afCubeValue[iVertex] = fSample(fX + a2fVertexOffset[iVertex][0]*fScale,
fY + a2fVertexOffset[iVertex][1]*fScale,
fZ + a2fVertexOffset[iVertex][2]*fScale);
}
//Find which vertices are inside of the surface and which are outside
iFlagIndex = 0;
for(iVertexTest = 0; iVertexTest < 8; iVertexTest++)
{
if(afCubeValue[iVertexTest] <= fTargetValue)
iFlagIndex |= 1<
亲测运行效果如下:
5.算法细节
这个算法有两个主要组成部分。第一个是决定如何定义切割单个立方体的截面或表面截面。如果我们将每个角分类为低于或高于等值,就有256种可能的角分类构型。其中两个是无关紧要的;所有点都在立方体内部或外部的位置对等值面没有影响。对于所有其他构型,我们需要确定在每个立方体边缘上等值面相交的位置,并使用这些边缘交点为等值面创建一个或多个三角形块。
如果考虑对称性,在剩下的254种可能中,实际上只有14种构型是唯一的。当只有一个角小于等值时,这就形成了一个三角形,它与在这个角上相交的边缘相交,而补丁法线朝向这个角。显然,这种类型有8个相关配置(例如,配置2 -你可能需要调整colormap来查看球体/像素之间的平面)。通过反转法线,我们得到8种构型它们有7个角比等值小。然而,我们并不认为这些是独一无二的。对于两个角小于等值的构型,有3种独特的构型(例如,构型12),这取决于这些角是否属于同一条边,属于立方体的同一面,或相对于彼此的对角位置。对于3个角小于等值的构型,也有3个独特的构型(例如构型14),这取决于是否有0、1或2个共享边(2个共享边会给你一个L形)。有7独特配置有4个角小于等值时,根据是否有0,2、3(3变体在这一点),或4共享边缘(例如配置30 -你可能需要调整颜色看到孤立(远)范围内的三角形/像素)。
每一种非平凡构型都会导致1到4个三角形被添加到等值面上。实际的顶点本身可以通过沿边的插值来计算,或者默认它们的位置在边的中间。插值位置显然会给你更好的阴影计算和更平滑的表面。
现在我们可以为单个体素创建表面补丁,我们可以将这个过程应用到整个体素。我们可以在板中处理体积,其中每个板由2个像素片组成。我们可以独立对待每个立方体,或者我们可以传播共享边的立方体之间的边相交。这种共享也可以在相邻的板之间完成,这增加了一些存储和复杂性,但节省了计算时间。边缘/顶点信息的共享也导致了一个更紧凑的模型,一个更易于插值着色。
6.总结
Marching Cubes是一种用于绘制立体数据中等值面的算法。基本概念是,我们可以通过立方体8个角的像素值来定义体素(立方体)。如果一个立方体中的一个或多个像素的值小于用户指定的等值,并且一个或多个像素的值大于这个值,我们知道体素必须贡献等值面的某个分量。通过确定立方体的哪些边与等值面相交,我们可以创建三角形块,将立方体划分为等值面内部区域和外部区域。通过连接等值面边界上所有立方体的小块,我们得到了一个表面表示法。



