会员: 密码:  免费注册 | 忘记密码 | 会员登录 网页功能: 加入收藏 设为首页 网站搜索  
游戏开发 > 程序设计 > 3D图形
nehe 32课 模型的绘制Brett Porter --freefall
发表日期:2007-03-22 15:01:18作者: 出处:  

                     nehe  32课 模型的绘制Brett Porter 
                                                    freefall
这个工程的源文件是从PortaLib3D库中提取出来的,我开发这个库是为了让使用者仅添加很少的代码就可以显示模型,既然你能信任这个库,那么你应该明白它是怎么工作的,本教程的目的就是帮助你明白这些代码。

教程中这部分PortaLib3D代码保留了我的版权声明,但这并不意味着你不能使用它们,它仅仅是表明如果你复制-粘贴这些代码到你的工程中,你必须付费,就是这样子了。如果你读懂理解了这些代码并且自己重新实现了这些代码的功能(如果你没有用这个库,我们鼓励你这样做,因为仅仅把代码复制到你的工程中,你将什么也学不到!),你就没有这个义务了,让我们来正视这些代码吧!它们并没有什么特别的,现在我们来看看一些更有趣的东西!

基本的OPENGL代码
这些基本的OPENGL代码放在Lesson32.cpp文件中,大部分来自第六课,仅仅在做纹理导入和绘制方面做了一些小的改动,这些改动将会在后边谈到。

Milkshape 3D模型
在这个例子中用的模型是Milkshape 3D,我之所以用它,是因为它是一种极好的模型包,它还包含了它的文件格式,所以很容易分析和理解,我的下一个计划是实现Anim8or 文件阅读器,因为它是免费的并且是一个3DS模型阅读器。
   我们简短的描述一下文件的格式,但这并不是我们最关心的。你必须创建你自己的适合于存储数据的结构体,然后把数据读入到结构体中。首先,让我们来描述一个模型所需要的结构体。

模型数据结构
  这些模型数据结构来源于Model.h文件中的Model类。首先,并且是最重要的是我们需要顶点:

// 顶点结构
struct Vertex
{
char m_boneID; // 用于骨骼动画
float m_location[3];
};

// 用到的顶点
int m_numVertices;
Vertex *m_pVertices;

目前,你可以忽略m_boneID这个变量---它会在以后的教程中介绍!m_location数组代表的是顶点(x,y,z)的坐标,这两个变量把点的数量和点存储在一个由导入程序分配的动态数组中。
下一步我们需要将点聚集成为三角形。

// 三角形结构体
struct Triangle
{
float m_vertexNormals[3][3];
float m_s[3], m_t[3];
int m_vertexIndices[3];
};
// 使用的三角形变量
int m_numTriangles;
Triangle *m_pTriangles;

现在,组成三角形的3个点存储在m_vertexIndices中,它们存储的是数组中相对于m_pVertices的偏移量(相当于数组的下标),这种存储方式每个点只需要被列出来一次,节省存储空间(当涉及到动画的时候也节省了计算量),m_s和m_t变量是每个点的纹理坐标(s,t),这些纹理将会被用到网格(将会在后边讨论)上。最后我们用m_vertexNormals来存放这三个点的法线。每条法线向量用3个浮点数来表示。

我们有的下一个结构体是一个网格,网格是一组有相同材质的三角形,网格的集合组成了整个模型,网格结构体如下:

// Mesh
struct Mesh
{
int m_materialIndex;
int m_numTriangles;
int *m_pTriangleIndices;
};

// Meshes Used
int m_numMeshes;
Mesh *m_pMeshes;

现在你可以用m_pTriangleIndices存储网格中的三角形,就象存储三角形用顶点坐标的偏移量一样,它需要动态分配空间因为我们无法预先知道三角形的数量,三角形的数量用m_numTriangles来指定。最后,m_materialIndex是网格材质的索引(纹理和光照的参数),我把材质结构体展示如下:

// Material Properties
struct Material
{
float m_ambient[4], m_diffuse[4], m_specular[4], m_emissive[4];
float m_shininess;
GLuint m_texture;
char *m_pTextureFilename;
};

// Materials Used
int m_numMaterials;
Material *m_pMaterials;

这里我们采用了与opengl标准的光照系数相同的格式:环境光、漫反射、镜面反射光、发散光、亮度。我们也有纹理对象和纹理文件的文件名(动态生成),有了这些材质在OpenGL上下文丢失的时候可以重新导入。

代码--导入模型

现在,我们就要导入模型了,你将会注意到一个名为loadModelData的纯虚函数,它带有一个模型名称的自变量,我们创建了一个导出类,MilkshapeModel,它实现了这个函数,并且把数据装到上面提到的保护数据结构中。我们现在来看看者个函数:

bool MilkshapeModel::loadModelData( const char *filename )
{
ifstream inputFile( filename, ios::in | ios::binary | ios::nocreate );
if ( inputFile.fail())
return false; // "Couldn't Open The Model File."

首先,文件被打开,它是一个二进制文件,因此使用了ios::binary限定词,如果文件没有被找到,函数返回false表明一个错误。

inputFile.seekg( 0, ios::end );
long fileSize = inputFile.tellg();
inputFile.seekg( 0, ios::beg );

上边这些代码决定了文件的字节数(大小);

byte *pBuffer = new byte[fileSize];
inputFile.read( pBuffer, fileSize );
inputFile.close();

然后整个文件都被读到了一个临时缓冲区中;

const byte *pPtr = pBuffer;
MS3DHeader *pHeader = ( MS3DHeader* )pPtr;
pPtr += sizeof( MS3DHeader );

if ( strncmp( pHeader->m_ID, "MS3D000000", 10 ) != 0 )
return false; // "Not A Valid Milkshape3D Model File."

if ( pHeader->m_version < 3 || pHeader->m_version > 4 )
return false; // "Unhandled File Version.  Only Milkshape3D
Version 1.3 And 1.4 Is Supported."

现在,我们需要一个指针来指出在文件中的当前位置,pPtr首先指向文件头部,然后指针向前移动,你会注意到我们用了很多的MS3D结构体,这些结构体在 MilkshapeModel.cpp文件的头部声明的,它们是直接来源于文件格式的说明。我们核对了文件的头部以确定我们所读的是一个正确的文件。

int nVertices = *( word* )pPtr; 
m_numVertices = nVertices;
m_pVertices = new Vertex[nVertices];
pPtr += sizeof( word );

int i;
for ( i = 0; i < nVertices; i++ )
{
MS3DVertex *pVertex = ( MS3DVertex* )pPtr;
m_pVertices[i].m_boneID = pVertex->m_boneID;
memcpy( m_pVertices[i].m_location, pVertex->m_vertex, sizeof( float )*3 );
pPtr += sizeof( MS3DVertex );
}

上边这段代码读入了文件中的点结构,首先给模型中的点结构分配内存,然后随着指针的移动每个点被分离了出来,在函数中很多地方调用了memcpy,
它能够很容易的把小块数组的内容复制出来。现在我们仍然忽略m_boneID变量--它仅仅用在骨骼动画上。

int nTriangles = *( word* )pPtr;
m_numTriangles = nTriangles;
m_pTriangles = new Triangle[nTriangles];
pPtr += sizeof( word );

for ( i = 0; i < nTriangles; i++ )
{
MS3DTriangle *pTriangle = ( MS3DTriangle* )pPtr;
int vertexIndices[3] = { pTriangle->m_vertexIndices[0], pTriangle->m_vertexIndices[1], pTriangle->m_vertexIndices[2] };
float t[3] = { 1.0f-pTriangle->m_t[0], 1.0f-pTriangle->m_t[1], 1.0f-pTriangle->m_t[2] };
memcpy( m_pTriangles[i].m_vertexNormals, pTriangle->m_vertexNormals, sizeof( float )*3*3 );
memcpy( m_pTriangles[i].m_s, pTriangle->m_s, sizeof( float )*3 );
memcpy( m_pTriangles[i].m_t, t, sizeof( float )*3 );
memcpy( m_pTriangles[i].m_vertexIndices, vertexIndices, sizeof( int )*3 );
pPtr += sizeof( MS3DTriangle );
}

对于点,函数的这部分存储了模型中的所有三角形。大部分代码所做的工作只是把数据从一个结构体复制到另一个结构体中,你会注意到点索引( vertexIndices )和
t 数组( t arrays)之间的差别。在文件中,点的索引存储的是一组字值(an array of word values),但在模型中为了保持一致性和简单性它是以整形存储的(不需要一些可怕的数据结构)。因此我们仅仅将3个值都转换成整形。t的值都被设置成了1.0(初始值)。这样做的原因是 OpenGL所采用的是左下坐标系,而MilkShape的纹理坐标所用的是左上坐标系。这就是说要把Y坐标倒置。

int nGroups = *( word* )pPtr;
m_numMeshes = nGroups;
m_pMeshes = new Mesh[nGroups];
pPtr += sizeof( word );
for ( i = 0; i < nGroups; i++ )
{
pPtr += sizeof( byte ); // Flags
pPtr += 32; // Name

word nTriangles = *( word* )pPtr;
pPtr += sizeof( word );
int *pTriangleIndices = new int[nTriangles];
for ( int j = 0; j < nTriangles; j++ )
{
pTriangleIndices[j] = *( word* )pPtr;
pPtr += sizeof( word );
}

char materialIndex = *( char* )pPtr;
pPtr += sizeof( char );

m_pMeshes[i].m_materialIndex = materialIndex;
m_pMeshes[i].m_numTriangles = nTriangles;
m_pMeshes[i].m_pTriangleIndices = pTriangleIndices;

上面这段代码导入了网格数据结构(在Milksharp3D中也被称为组)。因为每个网格中的三角形数量是不同的,没有标准的结构可以读取。我们就一块一块的读取这些数据.存放这些三角形的空间是在网格中动态分配的,并且一次读入。

int nMaterials = *( word* )pPtr;
m_numMaterials = nMaterials;
m_pMaterials = new Material[nMaterials];
pPtr += sizeof( word );
for ( i = 0; i < nMaterials; i++ )
{
MS3DMaterial *pMaterial = ( MS3DMaterial* )pPtr;
memcpy( m_pMaterials[i].m_ambient, pMaterial->m_ambient, sizeof( float )*4 );
memcpy( m_pMaterials[i].m_diffuse, pMaterial->m_diffuse, sizeof( float )*4 );
memcpy( m_pMaterials[i].m_specular, pMaterial->m_specular, sizeof( float )*4 );
memcpy( m_pMaterials[i].m_emissive, pMaterial->m_emissive, sizeof( float )*4 );
m_pMaterials[i].m_shininess = pMaterial->m_shininess;
m_pMaterials[i].m_pTextureFilename = new char[strlen( pMaterial->m_texture )+1];
strcpy( m_pMaterials[i].m_pTextureFilename, pMaterial->m_texture );
pPtr += sizeof( MS3DMaterial );
}

reloadTextures();

最后,材质信息从缓冲区中取出。采用的是和上边相同的方法,即复制每个光照参数到新的结构体中。同时为纹理文件名分配内存空间,并把它复制到该空间中。最后一句调用的reloadTextures()是用来真正导入纹理,并把它与OpenGL物体绑定在一起。这个函数来自Model base 类,我们会在后面讨论。

delete[] pBuffer;

return true;
}

最后的部分释放了复制数据所用的临时缓冲区并且表明成功(returns successfully)。到现在Model类中的保护成员变量中装入了模型的信息,你会注意到这些就是Milkshape3D中所有的代码了,这些代码是专门为Milkshape3D设计的。现在,在模型绘制之前,我们需要为模型中每一个物体导入它的纹理,这是由下面的代码来完成的:

void Model::reloadTextures()
{
for ( int i = 0; i < m_numMaterials; i++ )
if ( strlen( m_pMaterials[i].m_pTextureFilename ) > 0 )
m_pMaterials[i].m_texture = LoadGLTexture( m_pMaterials[i].m_pTextureFilename );
else
m_pMaterials[i].m_texture = 0;
}

对于每一个物体,我们都用NeHe的基本代码来导入它的纹理(仅仅在以前的版本上做了一些小的改动)。如果纹理文件名为空,那么就不会有纹理被导入,同时纹理标志符被置成0来表明没有纹理。

代码--绘制模型
现在我们来看绘制模型的代码!我们已经对内存中的数据结构有了一个很好的安排,因此绘制模型一点也不难了。

void Model::draw() 
{
GLboolean texEnabled = glIsEnabled( GL_TEXTURE_2D );

第一部分保存了OpenGL地图绘制的状态,我们的目的是不让程序改变它本来的状态,注意它并没有以这种方法保存物体的属性,现在我们循环并单独画出每一个网格:

// Draw By Group
for ( int i = 0; i < m_numMeshes; i++ )
{

m_pMeshes[i]被用来给当前的网格提供参考。现在,没一个网格都友它自己的材质属性了,我们可以通过它来建立起OpenGL状态。如果网格的materialIndex值为-1那么这个网格就没有材质,那么它就会用OpenGL的默认属性来绘制。

int materialIndex = m_pMeshes[i].m_materialIndex;
if ( materialIndex >= 0 )
{
glMaterialfv( GL_FRONT, GL_AMBIENT, m_pMaterials[materialIndex].m_ambient );
glMaterialfv( GL_FRONT, GL_DIFFUSE, m_pMaterials[materialIndex].m_diffuse );
glMaterialfv( GL_FRONT, GL_SPECULAR, m_pMaterials[materialIndex].m_specular );
glMaterialfv( GL_FRONT, GL_EMISSION, m_pMaterials[materialIndex].m_emissive );
glMaterialf( GL_FRONT, GL_SHININESS, m_pMaterials[materialIndex].m_shininess );

if ( m_pMaterials[materialIndex].m_texture > 0 )
{
glBindTexture( GL_TEXTURE_2D, m_pMaterials[materialIndex].m_texture );
glEnable( GL_TEXTURE_2D );
}
else
glDisable( GL_TEXTURE_2D );
}
else
{
glDisable( GL_TEXTURE_2D );
}

材质属性是通过存储在模型中的材质值来设定的。注意仅仅当materialIndex的值大于0时才能使材质有效,并把它和网格联系起来。如果它的值是0就直接取消该次绘制,表明并没有纹理,也就是将纹理绘制属性设置为无效,当整个网格都没有纹理的时候纹理绘制属性也将被设置为无效。

glBegin( GL_TRIANGLES );
{
for ( int j = 0; j < m_pMeshes[i].m_numTriangles; j++ )
{
int triangleIndex = m_pMeshes[i].m_pTriangleIndices[j];
const Triangle* pTri = &m_pTriangles[triangleIndex];

for ( int k = 0; k < 3; k++ )
{
int index = pTri->m_vertexIndices[k];

glNormal3fv( pTri->m_vertexNormals[k] );
glTexCoord2f( pTri->m_s[k], pTri->m_t[k] );
glVertex3fv( m_pVertices[index].m_location );
}
}
}
glEnd();
}

以上这部分绘制了模型中的三角形,它遍历了网格中的每一个三角形,绘制了每个三角形的三个顶点,并且绘制了法线和纹理坐标。网格中的每个三角形和三角形中的每个点一样都是以索引的方式保存在模型数组中的(这就是我们所用的两个索引变量)。pTri 是一个指向网格中当前三角形的指针,它可以用来简化后面的代码。

if ( texEnabled )
glEnable( GL_TEXTURE_2D );
else
glDisable( GL_TEXTURE_2D );
}

最后这部分代码把纹理属性恢复成了最初的状态了(运行该段代码之前的状态),在Model类中其它的比较有意思的代码就是同构和析构函数了,他们都是自解析(self explanatory)的。同构函数初始化所有的成员为0(指针设为NULL),析构函数删除所有为模型结构动态分配的内存。请注意如果你两次对同一个模型使用loadModelData()函数,那么就会发生内存泄露。最后我想要讨论的是要使用新模型类而对基本代码所做出的改变,从这些代码开始我将在后面的教程中介绍骨骼动画。

Model *pModel = NULL; // Holds The Model Data
在Lesson32.cpp中的顶部,模型被声明但没有初始化,它是在WinMain()中创建的。
pModel = new MilkshapeModel();
if ( pModel->loadModelData( "data/model.ms3d" ) == false )
{
MessageBox( NULL, "Couldn't load the model data/model.ms3d", "Error", MB_OK | MB_IConERROR );
return 0; // If Model Didn't Load, Quit
}

模型在这里被创建而没有在InitGL中创建的原因是:InitGL()可能在每个窗口被改变的时候调用(丢失OpenGL上下文的时候)。但这个时候模型并不需要被重新导入,因为它的数据还是完整的。不完整的仅仅是我们导入物体的纹理,所以下面的代码就加到了InitGL()中了。

pModel->reloadTextures();

这并不想我们过去使用LoadGLTextures一样。如果在场景中有多于一个的模型,那么这个函数必须为每个模型调用一次reloadTextures ()。如果你忽然得到一个白色的物体,那么就是你的纹理丢失了并且没有正确的被重新导入。最后这里友一个新的DrawGLScene()函数:
int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer
glLoadIdentity(); // Reset The View
gluLookAt( 75, 75, 75, 0, 0, 0, 0, 1, 0 );

glRotatef(yrot,0.0f,1.0f,0.0f);

pModel->draw();

yrot+=1.0f;
return TRUE; // Keep Going
}
简单吧!我们清除了颜色缓存,把矩阵设置成为模型/投影矩阵,然后用gluLookAt()函数设置了一个视觉投射(eye projection)。如果你以前没有用过gluLookAt()函数,那么本质上来说就是用前3个参数来设置照相机(别告诉我你不知道OpenGL中的照相机哈^_^)的位置,接下来的3个参数是用来把场景放置的位置(即设置你面对着的方向),最后的三个参数是一个“上”矢量(即相机是正着放还是斜着放)。在这个例子里,我们从(75,75,75)“看”到(0,0,0)-除非你在画之前移动了它,那么它就好象模型被画在(0,0,0)点处,并且正对着Y轴。这个函数必须在重置矩阵之前使用,在重置矩阵之后绘制所需的场景。

为了让它更加有趣一些,我们用glRotatef()函数让场景围绕着Y轴旋转。

最后,模型用绘制元函数(draw member function)绘制出来,它的绘制中点在原点上(我们假设它在Milkshape3D中也是绘制在原点上的!)所以如果你想要移动、旋转、放大它的话,你只需要在绘制它之前加上适当的OpenGL函数就可以了。你看!现在来测试一下----试着在 Milkshape中制作你自己的模型(或使用它的导入函数),然后在你的WinMain函数中代码做一些修改,把模型导入进来。或者把他们加到场景里面并且画上很多的模型!

接下来是什么呢?

在以后的NeHe的教程中,我将要说明怎样把这个类扩展,使得它能够兼容骨骼动画。如果我继续研究的话,我会写出更多的模型导入类,使得整个程序有更好的兼容性。

骨骼动画并没有看起来这么难,但是所涉及到的数学知识是很巧妙的。如果你还对矩阵和点还懂得不多,那么现在就是去读懂它们的时候了!在网上能够找到很多对你有帮助的资源的。

再见!

关于Brett Porter的一些信息:他现在是一个Java程序员,在IDM's gameplayNOW公司工作。出生在澳大利亚,在 Wollongong读完大学,最近刚读完BCompSc和BMath两个专业的研究生。他于12年前开始用BASIC编程,很快转向了Pascal,  Intel assembly, C++ 和 Java。在最近这几年3D编程变得很热门,他选择了OpenGL作为他的图形应用程序接口,想得到更多的信息请访问他的主页。

Brett Porter - Code / HTML
Jeff Molofee (NeHe) - HTML Modifications

返回顶部】 【打印本页】 【关闭窗口

关于我们 / 给我留言 / 版权举报 / 意见建议 / 网站编程QQ群   
Copyright ©2003- 2024 Lihuasoft.net webmaster(at)lihuasoft.net 加载时间 0.0032