以文本方式查看主题

-  计算机科学论坛  (http://bbs.xml.org.cn/index.asp)
--  『 C/C++编程思想 』  (http://bbs.xml.org.cn/list.asp?boardid=61)
----  [推荐]NeHe OpenGL教程(中英文版附带VC++源码)Lesson 31-lesson  32  (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=54209)


--  作者:一分之千
--  发布时间:10/23/2007 12:55:00 PM

--  [推荐]NeHe OpenGL教程(中英文版附带VC++源码)Lesson 31-lesson  32

第三十一三十二课源码

第三十一课


按此在新窗口浏览图片模型加载:

你知道大名鼎鼎的Milkshape3D建模软件么,我们将加载它的模型,当然你可以加载任何你认为不错的模型。

  
   
   
这篇渲染模型的文章是由Brett Porter所写的。
这篇教程的代码是从PortaLib3D中提取出来的,PortaLib3D是一个可以读取3D文件实用库。
这篇教程的代码是以第六课为基础的,我们只讨论改变的部分。

这课中使用的模型是从Milkshape3D中提取出来的,Milkshape3D是一个非常好的建模软件,它包含了自己的文件格式,所以你能很容易去分析和理解。

但是文件格式并不能使你加载一个模型,你必须自己定义一个结构去保存数据,接着把数据读入那个结构,我们将告诉你如何定义这样一个结构。

模型的定义在model.h中,好吧我们开始吧:
  
   

// 顶点结构
struct Vertex
{
 char m_boneID; // 顶点所在的骨骼
 float m_location[3];
};

// 顶点的个数和数据
int m_numVertices;
Vertex *m_pVertices;

   
在这一课你,你可以忽略m_boneID,我们将在以后的教程中介绍骨骼动画。m_location定义顶点的位置。
下面是三角形结构
  
   

// 三角形结构
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_s 和 m_t储存了三个顶点的纹理坐标。m_vertexNormals保存了三个顶点的法线。
下面我们定义网格结构
  
   

// 网格结构
struct Mesh
{
 int m_materialIndex;
 int m_numTriangles;
 int *m_pTriangleIndices;
};

// 使用的网格
int m_numMeshes;
Mesh *m_pMeshes;

   
m_pTriangleIndices指向包含在网格中三角形的数据,它是动态分配的。 m_materialIndex 指向了这个网格所用的材质。  
   

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

// 使用的纹理
int m_numMaterials;
Material *m_pMaterials;

   
这里我们使用与OpenGL中相对的材质。
下面的代码用来载入模型,我们通过重载loadModelData函数来实现它。

我们创建了一个新类MilkshapeModel,它是从Model继承而来的。  
   

bool MilkshapeModel::loadModelData( const char *filename )
{
 ifstream inputFile( filename, ios::in | ios::binary | ios::nocreate );
 if ( inputFile.fail())
  return 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; // 如果不是一个有效的MS3D文件则返回

 if ( pHeader->m_version < 3 || pHeader->m_version > 4 )
  return false; // 如果不能支持这种版本的文件,则返回失败
   
上面的文件读取文件头  
   

 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 );
 }

   
上面的代码读取顶点数据  
   

 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 );
 }

   
上面的代码用来读取三角形信息,因为MS3D使用窗口坐标系而OpenGL使用笛卡儿坐标系,所以需要反转每个顶点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 ); 
  pPtr += 32; 

  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;
 }

   
上面的代码填充网格结构  
   

 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();

   
上面的代码加载纹理数据  
   

 delete[] pBuffer;

 return true;
}

   
上面的代码设置好了一切参数,但纹理还没有载入内存,下面的代码完成这个功能。  
   

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;
}

   
有了数据,就可以写出绘制函数了,下面的函数根据模型的信息,按网格分组,分别绘制每一组的数据。  
   

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

 // 按网格分组绘制
 for ( int i = 0; i < m_numMeshes; i++ )
 {

  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 );
  }

  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();
 }

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

   
有了上面的函数,我们来看看如何使用它们。首先,我们定义一个MilkshapeModel类。  
   

 Model *pModel = NULL; // 定义一个指向模型类的指针

   
接着加载模型文件  
   

 pModel = new MilkshapeModel();
 if ( pModel->loadModelData( "data/model.ms3d" ) == false )
 {
  MessageBox( NULL, "不能加载data/model.ms3d文件", "加载错误", MB_OK | MB_ICONERROR );
  return 0;         // 返回失败
 }

   
接着载入纹理  
   

 pModel->reloadTextures();

   
完成了初始化操作,我们来实际绘制我们的模型  
   

int DrawGLScene(GLvoid)
{
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 情况缓存
 glLoadIdentity();    
 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;      //成功返回
}

   
简单吧?下一步我们该做什么?在以后的教程中,我将会加入骨骼动画的知识,到时候见吧!


--  作者:一分之千
--  发布时间:10/23/2007 12:56:00 PM

--  
Lesson 31
   
Model Rendering Tutorial by Brett Porter (brettporter@yahoo.com)

The source for this project has been extracted from PortaLib3D, a library I have written to enable users to do things like displaying models with very little extra code. But so that you can trust such a library, you should understand what it is doing, so this tutorial aims to help with that.

The portions of PortaLib3D included here retain my copyright notices. This doesn't mean they can't be used by you - it means that if you cut-and-paste the code into your project, you have to give me proper credit. That's all. If you choose to read, understand, and re-implement the code yourself (and it is what you are encouraged to do if you are not actually using the library. You don't learn anything with cut-and-paste!), then you free yourself of that obligation. Let's face it, the code is nothing special. Ok, let's get onto something more interesting!

OpenGL Base Code

The OpenGL base code is in Lesson32.cpp. Mostly it came from Lesson 6, with a small modification to the loading of textures and the drawing routine. The changes will be discussed later.

Milkshape 3D

The model I use in this example is from Milkshape 3D. The reason I use this is because it is a damn fine modelling package, and it includes its file-format so it is easy to parse and understand. My next plan is to implement an Anim8or (http://www.anim8or.com) file reader because it is free and of course a 3DS reader.

However, the file format, while it will be described briefly here, is not the major concern for loading a model. You must create your own structures that are suitable to store the data, and then read the file into that. So first, let's describe the structures required for a model.

Model Data Structures

These model data structures come from the class Model in Model.h. First, and most important, we need vertices:   
   

// Vertex Structure
struct Vertex
{
 char m_boneID; // For Skeletal Animation
 float m_location[3];
};

// Vertices Used
int m_numVertices;
Vertex *m_pVertices;

   
For now, you can ignore the m_boneID variable - that will come in a future tutorial! The m_location array represents the coordinate of the vertex (X,Y,Z). The two variables store the number of vertices and the actual vertices in a dynamic array which is allocated by the loader.

Next we need to group these vertices into triangles:   
   

// Triangle Structure
struct Triangle
{
 float m_vertexNormals[3][3];
 float m_s[3], m_t[3];
 int m_vertexIndices[3];
};

// Triangles Used
int m_numTriangles;
Triangle *m_pTriangles;

   
Now, the 3 vertices that make up the triangle are stored in m_vertexIndices. These are offsets into the array of m_pVertices. This way each vertex need only be listed once, saving memory (and calculations when it comes to animating later). m_s and m_t are the (s,t) texture coordinates for each of the 3 vertices. The texture used is the one applied to this mesh (which is described next). Finally we have the m_vertexNormals member which stores the normal to each of the 3 vertices. Each normal has 3 float coordinates describing the vector.

The next structure we have in a model is a mesh. A mesh is a group of triangles that all have the same material applied to them. The collection of meshes make up the entire model. The mesh structure is as follows:   
   

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

// Meshes Used
int m_numMeshes;
Mesh *m_pMeshes;

   
This time you have m_pTriangleIndices storing the triangles in the mesh in the same way as the triangle stored indicies to its vertices. It will be dynamically allocated because the number of triangles in a mesh is not known in advance, and is specified by m_numTriangles. Finally, m_materialIndex is the index of the material (texture and lighting coeffecients) to use for the mesh. I'll show you the material structure below:   
   

// 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;

   
Here we have all the standard lighting coeffecients in the same format as OpenGL: ambient, diffuse, specular, emissive and shininess. We also have the texture object m_texture and the filename (dynamically allocated) of the texture so that it can be reloaded if the OpenGL context is lost.

The Code - Loading the Model

Now, on to loading the model. You will notice there is a pure virtual function called loadModelData, which takes the filename of the model as an argument. What happens is we create a derived class, MilkshapeModel, which implements this function, filling in the protected data structures mentioned above. Lets look at that function now:   
   

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."

   
First, the file is opened. It is a binary file, hence the ios::binary qualifier. If it is not found, the function returns false to indicate an error.   
   

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

   
The above code determines the size of the file in bytes.   
   

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

   
Then the file is read into a temporary buffer in its entirety.   
   

 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."

   
Now, a pointer is acquired to out current position in the file, pPtr. A pointer to the header is saved, and then the pointer is advanced past the header. You will notice several MS3D... structures being used here. These are declared at the top of MilkshapeModel.cpp, and come directly from the file format specification. The fields of the header are checked to make sure that this is a valid file we are reading.   
   

 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 );
 }

   
The above code reads each of the vertex structures in the file. First memory is allocated in the model for the vertices, and then each is parsed from the file as the pointer is advanced. Several calls to memcpy will be used in this function, which copies the contents of the small arrays easily. The m_boneID member can still be ignored for now - its for skeletal animation!   
   

 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 );
 }

   
As for the vertices, this part of the function stores all of the triangles in the model. While most of it involves just copying the arrays from one structure to another, you'll notice the difference for the vertexIndices and t arrays. In the file, the vertex indices are stores as an array of word values, but in the model they are int values for consistency and simplicity (no nasty casting needed). So this just converts the 3 values to integers. The t values are all set to 1.0-(original value). The reason for this is that OpenGL uses a lower-left coordinate system, whereas Milkshape uses an upper-left coordinate system for its texture coordinates. This reverses the y coordinate.   
   

 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;
 }

   
The above code loads the mesh data structures (also called groups in Milkshape3D). Since the number of triangles varies from mesh to mesh, there is no standard structure to read. Instead, they are taken field by field. The memory for the triangle indices is dynamically allocated within the mesh and read one at a time.   
   

 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();

   
Lastly, the material information is taken from the buffer. This is done in the same way as those above, copying each of the lighting coefficients into the new structure. Also, new memory is allocated for the texture filename, and it is copied into there. The final call to reloadTextures is used to actually load the textures and bind them to OpenGL texture objects. That function, from the Model base class, is described later.   
   

 delete[] pBuffer;

 return true;
}

   
The last fragment frees the temporary buffer now that all the data has been copied and returns successfully.

So at this point, the protected member variables of the Model class are filled with the model information. You'll note also that this is the only code in MilkshapeModel because it is the only code specific to Milkshape3D. Now, before the model can be rendered, it is necessary to load the textures for each of its materials. This is done with the following code:   
   

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;
}

   
For each material, the texture is loaded using a function from NeHe's base code (slightly modified from it's previous version). If the texture filename was an empty string, then it is not loaded, and instead the texture object identifier is set to 0 to indicate there is no texture.

The Code - Drawing the Model

Now we can start the code to draw the model! This is not difficult at all now that we have a careful arrangement of the data structures in memory.   
   

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

   
This first part saves the state of texture mapping within OpenGL so that the function does not disturb it. Note however that it does not preserve the material properties in the same way.

Now we loop through each of the meshes and draw them individually:   
   

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

   
m_pMeshes[i] will be used to reference the current mesh. Now, each mesh has its own material properties, so we set up the OpenGL states according to that. If the materialIndex of the mesh is -1 however, there is no material for this mesh and it is drawn with the OpenGL defaults.   
   

  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 );
  }

   
The material properties are set according to the values stored in the model. Note that the texture is only bound and enabled if it is greater than 0. If it is set to 0, you'll recall, there was no texture, so texturing is disabled. Texturing is also disabled if there was no material at all for the mesh.   
   

  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();
 }

   
The above section does the rendering of the triangles for the model. It loops through each of the triangles for the mesh, and then draws each of its three vertices, including the normal and texture coordinates. Remember that each triangle in a mesh and likewise each vertex in a triangle is indexed into the total model arrays (these are the two index variables used). pTri is a pointer to the current triangle in the mesh used to simplify the code following it.   
   

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

   
This final fragment of code sets the texture mapping state back to its original value.

The only other code of interest in the Model class is the constructor and destructor. These are self explanatory. The constructor initializes all members to 0 (or NULL for pointers), and the destructor deletes the dynamic memory for all of the model structures. You should note that if you call the loadModelData function twice for one Model object, you will get memory leaks. Be careful!

The final topic I will discuss here is the changes to the base code to render using the new Model class, and where I plan to go from here in a future tutorial introducing skeletal animation.   
   

 Model *pModel = NULL; // Holds The Model Data

   
At the top of the code in Lesson32.cpp the model is declared, but not initialised. It is created in 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
 }

   
The model is created here, and not in InitGL because InitGL gets called everytime we change the screen mode (losing the OpenGL context). But the model doesn't need to be reloaded, as its data remains intact. What doesn't remain intact are the textures that were bound to texture objects when we loaded the object. So the following line is added to InitGL:   
   

 pModel->reloadTextures();

   
This takes the place of calling LoadGLTextures as we used to. If there was more than one model in the scene, then this function must be called for all of them. If you get white objects all of a sudden, then your textures have been thrown away and not reloaded correctly.

Finally there is a new DrawGLScene function:   
   

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
}

   
Simple? We clear the colour buffer, set the identity into the model/view matrix, and then set an eye projection with gluLookAt. If you haven't used gluLookAt before, essentially it places the camera at the position of the first 3 parameters, places the center of the scene at the position of the next 3 parameters, and the last 3 parameters describe the vector that is "up". In this case, we look from (75, 75, 75) to (0,0,0) - as the model is drawn about (0,0,0) unless you translate before drawing it - and the positive Y-axis is facing up. The function must be called first, and after loading the identity to behave in this fashion.

To make it a bit more interesting, the scene gradually rotates around the y-axis with glRotatef.

Finally, the model is drawn with its draw member function. It is drawn centered at the origin (assuming it was modelled around the origin in Milkshape 3D!), so If you want to position or rotate or scale it, simply call the appropriate GL functions before drawing it. Voila! To test it out - try making your own models in Milkshape (or use its import function), and load them instead by changing the line in WinMain. Or add them to the scene and draw several models!

What Next?

In a future tutorial for NeHe Productions, I will explain how to extend this class structure to incorporate skeletal animation. And if I get around to it, I will write more loader classes to make the program more versatile.

The step to skeletal animation is not as large as it may seem, although the math involved is much more tricky. If you don't understand much about matrices and vectors, now is the time to read up them! There are several resources on the web that can help you out.

See you then!

Some information about Brett Porter: Born in Australia, he studied at the University of Wollongong, recently graduating with a BCompSc and a BMath. He began programming in BASIC 12 years ago on a Commodore 64 "clone" called the VZ300, but soon moved up to Pascal, Intel assembly, C++ and Java. During the last few years 3D programming has become an interest and OpenGL has become his graphics API of choice. For more information visit his homepage at: http://rsn.gamedev.net

A follow up to this tutorial on Skeletal Animation can be found on Brett's homepage. Visit the link above!

Brett Porter

Jeff Molofee (NeHe)


--  作者:一分之千
--  发布时间:10/23/2007 1:00:00 PM

--  

32 ke


按此在新窗口浏览图片拾取, Alpha混合, Alpha测试, 排序:

这又是一个小游戏,交给的东西会很多,慢慢体会吧

  
   
   
欢迎来到32课. 这课大概是在我所写作已来最大的一课. 超过1000 行代码和约1540行的HTML. 这也是第一课用到我新的NeHeGL 基本代码. 这课写了很长时间, 但我想他是值得期待的. 一些知识点用到是: Alpha 混合, Alpha 测试, 读取鼠标, 同时用到Ortho 和透视, 显示客户鼠标, 按深度排列物体, 动画帧从单张材质图 和更多要点, 你将学到更多精选的内容!
最初的版本是在屏幕上显示三个物体,当你单击他们将改变颜色. 很有趣!?! 不怎样! 象往常一样, 我想给你们这些家伙留下一个超极好的课程. 我想使课程有趣, 丰富,当然..美观. 所以, 经过几个星期的编码之后, 这课程完成了! 即使你不编码,你仍会喜欢这课. 这是个完整的游戏. 游戏的目标是射击更多的靶子, 在你失去一定数的靶子后,你将不能再用鼠标单击物体.

我确信会有批评,但我非常乐观对这课! 我已在从深度里选择和排序物体这个主题里找到快乐!

一些需要注意的代码. 我仅仅会在lesson32.cpp里讨论. 有一些不成熟的改动在 NeHeGL 代码里. 最重要的改动是我加入鼠标支持在 WindowProc(). 我也加入 int mouse_x, mouse_y 在存鼠标运动. 在 NeHeGL.h 以下两条代码被加入: extern int mouse_x; & extern int mouse_y;

课程用到的材质是用 Adobe Photoshop 做的. 每个 .TGA 文件是32位图片有一个alpha 通道. 若你不确信自已能在一个图片加入alpha通道, 找一本好书,上网,或读 Adobe Photoshop帮助. 全部的过程非常相似,我做了透明图在透明图课程. 调入你物体在 Adobe Photoshop (或一些其它图形处理程序,且支持alpha 通道). 用选择颜色工具选你图片的背景. 复制选区. 新建一个图. 粘贴生成新文件. 取消图片选择,你图的背景应是黑色. 使周围是白色. 选全部图复制. 回到最初的图且建一个alpha 通道. 粘贴黑和白透明图你就完成建立alpha通道.存图片为32位t .TGA文件. 使确定保存透明背景是选中的,保存!

如以往我希望你喜欢这课程. 我感兴趣你对他的想法. 若你有些问题或你发现一些问题,告诉我. 我匆忙的完成这课程 所以若你发现哪部分很难懂,给我发些邮件,然后我会用不同的方式或更详细的解释!

  
   

#include <windows.h>        
#include <stdio.h>        
#include <stdarg.h>        
#include <time.h>        
#include "NeHeGL.h"       
   
在第1课里, 我提倡关于适当的方法连接到 OpenGL 库. 在 Visual C++ 里点击’项目’,设置,连接项. 移下到 对象/库 模块 加入 OpenGL32.lib, GLu32.lib 和 GLaux.lib. 预编译一个需要的库的失败将使编译器找出所出的错误. 有时你不想发生! 使事情更坏, 若你仅仅预编译库在debug 模式, 和有人试在release 模式建立你程序... 更多的错误. 有许多人看代码. 他们大多数是新程序员. 他们取到你的代码, 试着编译. 他们得到错误, 删掉代码,移走.
下而的代码告诉编译者去连接需要的库. 一点多些的字, 但少些以后的头痛. 在这个课程, 我们将连接 OpenGL32 库,GLu32库 和 WinMM库 (用来放音乐). 在这课程我们会调入 .TGA 文件,所以我们不用 GLaux库.

  
   

#pragma comment( lib, "opengl32.lib" )      // 在链接时连接Opengl32.lib库
#pragma comment( lib, "glu32.lib" )       // 链接glu32.lib库
#pragma comment( lib, "winmm.lib" )       // 链接winmm.lib库

   
下而的3 行检查若 CDS_FULLSCREEN 已被你的编译器定义. 若还没被定义, 我们给 CDS_FULLSCREEN 为 4. 马上你完全部丢掉... 一些编译器不给 CDS_FULLSCREEN 变量,将返回一个错误,但是 CDS_FULLSCREEN 是有用的! 防止出错消息, 我们检查若 CDS_FULLSCREEN 是否定义,若出错, 我们定义他. 使每人生活更简单.
我们再定义 DrawTargets函数, 为窗口和按键设变量. 你若不懂定义,读一遍MSDN术语表.保持清醒, 我不是教 C/C++, 买一本好书若你对非gl代码要帮助!

  
   

#ifndef  CDS_FULLSCREEN       
#define  CDS_FULLSCREEN 4      
#endif         

void DrawTargets();        

GL_Window* g_window;
Keys*  g_keys;

   
下面的代码是用户设置变量. base 是将用到的字体显示列表的开始列表值. roll 是将用到的移动的大地和建立旋转的云. level 应是级别 (我们开始是 1级). miss 保留失去了多少物体. 他还用来显示用户的士气(不丢失意味着高士气). kills 保留每级打到多少靶子. score 会保存运行时打中的总数, 同时用到结束比赛!
最后一行是让我们通过结构比较的函数. 是等待qsort 最后参数到 type (const *void, const *void).

  
   

// 用户定义的变量
GLuint  base;        // 字体显示列表
GLfloat  roll;        // 旋转的云
GLint  level=1;        // 现在的等级
GLint  miss;        // 丢失的数
GLint  kills;        // 打到的数
GLint  score;        // 当前的分数
bool  game;        // 游戏是否结束?

typedef int (*compfn)(const void*, const void*);     // 定义用来比较的函数

   
现在为我们物体的结构. 这个结构存了所有一个物体的信息. 旋转的方向, 若被打中, 在屏幕的位置, 等等.
一个快速运动的变量... rot 我想让物体旋转特别的方向. hit 若物体没被打中将是 FALSE . 若物体给打中或飞出, 变量将是 TRUE.

变量frame 是用来存我们爆炸动画的周期. 每一帧改变增加一个爆炸材质. 在这课有更多在不久.

保存单个物体的移动方向, 我们用变量 dir. 一个dir 能有4 个值: 0 - 物体左移, 1 - 物体右移, 2 - 物体上移 和最后 3 - 物体下移

texid 能是从0到4的数. 0 表示是蓝面材质, 1 是水桶材质, 2 是靶子的材质 , 3 是 可乐的材质 和 4 是 花瓶 材质. 最近在调入材质的代码, 你将看到先前5种材质来自目标图片.

x 和 y 两者都用来记屏模上物体的位置. x 表示物体在 x-轴, y 表示物体在 y-轴.

物体在z-轴上的旋转是记在变量spin. 在以后的代码, 我们将加或减spin基数在旅行的方向上.

最后, distance 保存我们物体到屏幕的距离. 距离是极端重要的变量, 我们将用他来计算屏幕的左右两边, 而且在对象关闭之前排序物体,画出物体的距离.

  
   

struct objects {
 GLuint rot;        // 旋转 (0-不转, 1-顺时针转, 2-逆时针转)
 bool hit;        // 物体碰撞?
 GLuint frame;        // 当前爆炸效果的动画帧
 GLuint dir;        // 物体的方向 (0-左, 1-右, 2-上, 3-下)
 GLuint texid;        // 物体材质 ID
 GLfloat x;        // 物体 X 位置
 GLfloat y;        // 物体 Y 位置
 GLfloat spin;        // 物体旋转
 GLfloat distance;        // 物体距离
};

   
解释下面的代码没有真正的结果. 我们在这课调入TGA图代替bitmaps图片. 下面的用来表示TGA图片数据的结构是尽可能好的 . 若你需要详细的解释下面的代码,请读关于调入TGA 文件的课程.   
   

typedef struct         // 新建一个结构
{
 GLubyte *imageData;       // 图片数据 (最大 32 位)
 GLuint bpp;        // 图片颜色深度 每象素
 GLuint width;        // 图片宽度
 GLuint height;        // 图片高度
 GLuint texID;        // 贴图材质 ID 用来选择一个材质
} TextureImage;         // 结构 名称

   
紧接下面的代码为们10个材质和个30物体留出空间. 若你打算在游戏里加更多物体,得增加这个变量到你想到的数  
   

TextureImage textures[10];       // 定义10个材质

objects object[30];       // 定义 30 个物体

   
我不想限制每个物体的大小. 我想瓶子(vase)比can高, 我想水桶bucket比瓶子宽. 去改变一切是简单的, 我建了一个结构存物体的宽和高.
我然后在最后一行代码中设每个物体的宽高. 得到这个coke cans的宽, 我将检查size[3].w. 蓝面是 0, 水桶是 1, 和靶子是 2, 等. 宽度表现在 w. 使有意义?

  
   

struct dimensions {        // 物体维数
 GLfloat w;        // 物体宽
 GLfloat h;        // 物体高
};

// 每个物体的大小: 蓝面, 水桶, 靶子, 可乐, 瓶子
dimensions size[5] = { {1.0f,1.0f}, {1.0f,1.0f}, {1.0f,1.0f}, {0.5f,1.0f}, {0.75f,1.5f} };

   
下面是大段代码是调入我们 TGA 图片和转换他为材质. 这是同我在第25课所用的一样的代码,你可回去看一看.
我用TGA 图片的原因是他们是有alpha 通道的. 这个alpha 通道告诉 OpenGL 哪一部分图是透明的,哪一部分是白底. alpha 通道是被建立在图片处理程序, 并保存在.TGA图片里面. OpenGL 调入图片, 能用alpha 通道设置图片中每个象素透明的数量.

  
   

bool LoadTGA(TextureImage *texture, char *filename)     // 调入一个TGA 文件到内存
{
 GLubyte  TGAheader[12]={0,0,2,0,0,0,0,0,0,0,0,0};   // (未)压缩的 TGA 头
 GLubyte  TGAcompare[12];      // 用来比较 TGA 头
 GLubyte  header[6];      // 首先 6 个有用的字节
 GLuint  bytesPerPixel;      // 每象素字节数在 TGA 文件使用
 GLuint  imageSize;      // 用来图片大小的存储
 GLuint  temp;       // 临时变量
 GLuint  type=GL_RGBA;      // 设置默认的 GL 模式 为 RBGA

 FILE *file = fopen(filename, "rb");      // 打开 TGA 文件

 if( file==NULL ||       // 文件是否已存在 ?
  fread(TGAcompare,1,sizeof(TGAcompare),file)!=sizeof(TGAcompare) || // 是否读出12个字节?
  memcmp(TGAheader,TGAcompare,sizeof(TGAheader))!=0 ||   // 文件头是不是我们想要的 ?
  fread(header,1,sizeof(header),file)!=sizeof(header))   // 若正确则读下 6 个 Bytes
 {
  if (file == NULL)       // 文件是否已存在 ?
   return FALSE;      // 返回错误
  else        // 否则
  {
   fclose(file);      // 若有任何错误, 关掉文件
   return FALSE;      // 返回错误
  }
 }

 texture->width  = header[1] * 256 + header[0];    // 定义 TGA 宽
 texture->height = header[3] * 256 + header[2];    // 定义 TGA 高

 if( texture->width <=0 ||      // 若 宽<=0
  texture->height <=0 ||      // 若 高<=0
  (header[4]!=24 && header[4]!=32))     // 若 TGA 是 24 or 32 位?
 {
  fclose(file);       // 若有任何错误, 关掉文件
  return FALSE;       // 返回错误
 }

 texture->bpp = header[4];      // 取 TGA 的位每象素 (24 或 32)
 bytesPerPixel = texture->bpp/8;      // 除以 8 得到字节每象素
 imageSize  = texture->width*texture->height*bytesPerPixel;  // 计算 所需内存为 TGA 数据

 texture->imageData=(GLubyte *)malloc(imageSize);    // 分配 内存 为 TGA 数据

 if( texture->imageData==NULL ||      // 这个内存是否存在?
  fread(texture->imageData, 1, imageSize, file)!=imageSize)  // 图片大小与保留内存的大小想等 ?
 {
  if(texture->imageData!=NULL)     // 图片数据的调入
   free(texture->imageData);     // 若成功, 释放图象数据

  fclose(file);       // 关掉文件
  return FALSE;       // 返回错误
 }

 for(GLuint i=0; i<int(imageSize); i+=bytesPerPixel)    // 在图象数据里循环
 {         // 交换第1和第3 Bytes (’红’red 和 ’蓝’blue)
  temp=texture->imageData[i];      // 临时存储 图象的 ’i’
  texture->imageData[i] = texture->imageData[i + 2];   // 设 第1 Byte 得到变量 第3 Byte
  texture->imageData[i + 2] = temp;     // 设第3 Byte 得到变量 ’temp’ (第1 Byte 变量)
 }

 fclose (file);        // 关掉文件

 // 建立一个贴图材质从以上数据
 glGenTextures(1, &texture[0].texID);      // 生成 OpenGL 材质 ID

 glBindTexture(GL_TEXTURE_2D, texture[0].texID);    // 绑定我们的材质
 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);  // 线过滤器
 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  // 线过滤器

 if (texture[0].bpp==24)       // 若 TGA 是24 位的
 {
  type=GL_RGB;       // 设 ’type’ 为 GL_RGB
 }

 glTexImage2D(GL_TEXTURE_2D, 0, type, texture[0].width, texture[0].height, 0, type, GL_UNSIGNED_BYTE, texture[0].imageData);

 return true;        // 材质建立成功, 返回正确
}

   
2D 字体材质代码同我已在前一课用的是一样的. 然而, 有一些小不同. 第一是你将看到仅仅唯一生成95 显示列表. 若你看字体材质, 你会看到只有 95 字母计算空间在图片顶,左. 第二个事是你将通知分在16.0f 为 cx 和 我们只分在8.0f 为cy. 我这样做的结果是因为字体材质是256 象素宽, 但仅仅一伴就高(128 象素). 所以计算cx 我们分为16.0f 和计算分cy 为一半(8.0f).
若你不懂下面的代码, 回去读17课. 建立字体的代码的详细解释在第17课里!

  
   

GLvoid BuildFont(GLvoid)       // 建立我们字体显示列表
{
 base=glGenLists(95);      // 建立 95 显示列表
 glBindTexture(GL_TEXTURE_2D, textures[9].texID);   // 绑我们字体材质
 for (int loop=0; loop<95; loop++)     // 循环在 95 列表
 {
  float cx=float(loop%16)/16.0f;    // X 位置 在当前字母
  float cy=float(loop/16)/8.0f;    // Y 位置 在当前字母

  glNewList(base+loop,GL_COMPILE);    // 开始建立一个列表
   glBegin(GL_QUADS);     // 用四边形组成每个字母
    glTexCoord2f(cx,         1.0f-cy-0.120f); glVertex2i(0,0); // 质地 / 点 座标 (底 左)
    glTexCoord2f(cx+0.0625f, 1.0f-cy-0.120f); glVertex2i(16,0); // 质地 / 点 座标 (底 右)
    glTexCoord2f(cx+0.0625f, 1.0f-cy);   glVertex2i(16,16); // 质地 / 点 座标 (顶 右)
    glTexCoord2f(cx,         1.0f-cy);   glVertex2i(0,16); // 质地 / 点 座标 (顶 左)
   glEnd();      // 完成建立我们的 四边形 (字母)
   glTranslated(10,0,0);    // 移到字体的右边
  glEndList();      // 完成建军立这个显示列表
 }        // 循环直到所有 256 完成建立
}

   
输出的代码也在第17课, 但已修改为在屏幕输出我们的分数, 等级和士气(不断改变的值).   
   

GLvoid glPrint(GLint x, GLint y, const char *string, ...)    // 输出在屏慕的位置
{
 char  text[256];      // 保存在我们的字符串
 va_list  ap;       // 到列表的指针

 if (string == NULL)       // 若文字为空
  return;        // 返回

 va_start(ap, string);       // 解析字符串
     vsprintf(text, string, ap);      // 转换字符串
 va_end(ap);        // 结果的字符串

 glBindTexture(GL_TEXTURE_2D, textures[9].texID);    // 选择我们字体材质
 glPushMatrix();        // 存观看模式矩阵
 glLoadIdentity();        // 设观看模式矩阵
 glTranslated(x,y,0);       // 文字输出位置 (0,0 - 底 左-Bottom Left)
 glListBase(base-32);       // 选择字体设置
 glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);    // 输出显示列表中的文字
 glPopMatrix();        // 取出以前的模式矩阵
}

   
这些代码调用排序程序. 它比较距离在两个结构并返回-1 若第一个结构的距离小于第二个 , 1 i若 第一个结构的距离大于第二个 0 否则 (若 距离相等)   
   

int Compare(struct objects *elem1, struct objects *elem2)   // 比较 函数
{
   if ( elem1->distance < elem2->distance)     // 若 第一个结构的距离小于第二个
      return -1;        // 返回 -1
   else if (elem1->distance > elem2->distance)    // 若 第一个结构的距离大于第二个
      return 1;        // 返回1
   else         // 否则 (若 距离相等)
      return 0;        // 返回 0
}

   
InitObject() 代码是来建立每个物体. 我们开始设 rot 为 1. 这使物体顺时针旋转. 然后设爆炸效果动画帧为0(我们不想爆炸效果从中间开始).我们下面设 hit 为 FALSE, 意思是物体还没被击中或正开如. 选一个物体材质, texid 用来给一个随机的变量从 0 到 4. 0是blueface 材质 和4 是 vase 材质. 这给我们5种随机物体.
距离变量是在-0.0f to -40.0f (4000/100 is 40)的随机数 . 当我们真实的画物体,我们透过在屏幕上的另10 个单位. 所以当物体在画时, 他们将画从-10.0f to -50.0f 单位 在屏幕(不挨着, 也不离得太远). 我分随机数为 100.0f 得到更精确的浮点数值.

在给完随机的距离之后, 我们给物体一个随机的 y . 我们不想物体低于 -1.5f, 否则他将低于大地, 且我们不想物体高于3.0f. 所以留在我们的区间的随机数不能高于4.5f (-1.5f+4.5f=3.0f).

去计算 x 位置, 用一些狡猾的数学. 用我们的距离减去15.0f . 除以2 减5*level. 再 减随机数(0.0f 到5) 乘level. 减 5*level rndom(0.0f to 5*level) 这是最高级.

选一个方向.

使事情简单明白x, 写一个快的例子. 距离是 -30.0f ,当前级是 1:

object[num].x=((-30.0f-15.0f)/2.0f)-(5*1)-float(rand()%(5*1));
object[num].x=(-45.0f/2.0f)-5-float(rand()%5);
object[num].x=(-22.5f)-5-{lets say 3.0f};
object[num].x=(-22.5f)-5-{3.0f};
object[num].x=-27.5f-{3.0f};
object[num].x=-30.5f;

开始在屏模上移 10 个单位 , 距离是 -30.0f. 其实是 -40.0f.用透视的代码在 NeHeGL.cpp 文件.


  
   

GLvoid InitObject(int num)       // 初始化一个物体
{
 object[num].rot=1;       // 顺时针旋转
 object[num].frame=0;      // 设爆炸效果动画帧为0
 object[num].hit=FALSE;      // 设点击检测为0
 object[num].texid=rand()%5;      // 设一个材质
 object[num].distance=-(float(rand()%4001)/100.0f);   // 随机距离
 object[num].y=-1.5f+(float(rand()%451)/100.0f);   // 随机 Y 位置
 // 随机开始 X 位置 基于物体的距离 和随机的延时量 (确定变量)
 object[num].x=((object[num].distance-15.0f)/2.0f)-(5*level)-float(rand()%(5*level));
 object[num].dir=(rand()%2);      // 选一个随机的方向

   
检查方向
  
   


 if (object[num].dir==0)       // 若随机的方向正确
 {
  object[num].rot=2;       // 逆时针旋转
  object[num].x=-object[num].x;     // 开始在左边 (否定 变量)
 }

   
现在我们检查texid 来找出所选的的物体. 若 texid 为0, 所选的物体是 Blueface . blueface 总是在大地上面旋转. 确定开始时在地上的层, 我们设 y 是 -2.0f.
  
   


 if (object[num].texid==0)       // 蓝色天空表面
  object[num].y=-2.0f;      // 总是在大地上面旋转

   
下面检查若texid 是 1. 这样, 电脑所选物体的是 Bucket. bucket不从左到右运动, 它从天上掉下来. 首先我们不得不设 dir 是 3. 这告诉电脑我们的水桶bucket 是掉下来或向下运动.
我们最初的代码假定物体从左到右运动. 因为bucket 是向下落的, 我们得不给它一个新的随机的变量 x . 若不是这样, bucket 会被看不到. 它将不在左边落下就在屏幕外面. 我们给它一个新的随机距离变量在屏幕上. 代替减去15, 我们仅仅减去 10. 这给我们一些幅度, 保持物体在屏幕??. 设我们的distance 是-30.0f, 从0.0f -40.0f的随机变量. 为什么从 0.0f 到 40.0f? 不是从0.0f to -40.0f? 答案很简单. rand() 函数总返回正数. 所以总是正数. 另外,回到我们的故事. 我们有个正数 从0.0f 到 40.0f.我们加距离 最小 10.0f 除以 2. 举个例子,设x变量为 15 ,距离是 -30.0f:

object[num].x=float(rand()%int(-30.0f-10.0f))+((-30.0f-10.0f)/2.0f);

object[num].x=float(rand()%int(-40.0f)+(-40.0f)/2.0f);

object[num].x=float(15 {assuming 15 was returned))+(-20.0f);

object[num].x=15.0f-20.0f;

object[num].x=-5.0f;

下面设y. 我们想水桶从天上下来. 我人不想穿过云. 所以我们设 y 为 4.5f. 刚在去的下面一点.

  
   


 if (object[num].texid==1)      // 水桶(Bucket)
 {
  object[num].dir=3;      // 下落
  object[num].x=float(rand()%int(object[num].distance-10.0f))+((object[num].distance-10.0f)/2.0f);
  object[num].y=4.5f;     // 随机 X, 开始在屏模上方
 }

   
我们想靶子从地面突出到天上. 我们检查物体为 (texid 是 2). 若是, 设方向(dir) 是 2 (上). 用精确的数 x 位置.
我们不想target 开始在地上. 设 y 初值为-3.0f (在地下). 然后减一个值从0.0f 到 5 乘当前 level. 靶子不是立即出现. 在高级别是有延时, 通过delay, 靶子将出现在一个在另一个以后, 给你很少时间打到他们.

  
   

 if (object[num].texid==2)      // 靶子
 {
  object[num].dir=2;      // 开始向上飞
  object[num].x=float(rand()%int(object[num].distance-10.0f))+((object[num].distance-10.0f)/2.0f);
  object[num].y=-3.0f-float(rand()%(5*level));   // 随机 X, 开始在下面的大地 + 随机变量
 }

   
所有其它的物体从右到左旅行, 因而不必给任何变量付值来改变物体. 它们应该刚好工作在所给的随机变量.
现在来点有趣的材料! "为了alpha 混合技术正常的工作, 透明的原物必须不断地排定在从后向前画". 当画alpha 混合物体是, 在远处的物体是先画的,这是非常重要的, 下面画紧临的上面的物体.

理由是简单的... Z 缓冲区防止 OpenGL 从已画好的混合东西再画象素. 这就是为什么会发生物体画在透明混合之后而不再显示出来. 为什么你最后看到的是一个四边形与物体重叠... 很不好看!

我们已知道每个物体的深度. 因而在初始化一个物体之后, 我们能通过把物体排序,而用qsort 函数(快速排序sort),来解决这个问题 . 通过物体排序, 我们能确信第一个画的是最远的物体. 这意味着当我们画物体时, 起始于第一个物体, 物体通过用距离将被先画. 紧挨着那个物体(晚一会儿画) 将看到先前的物体在他们的后面, 再将适度的混合!

这文中的这行线注释是我在 MSDN 里发现这些代码,在网上花时间查找之后找到的解答 . 他们工作的很好,允许各种的排序结构. qsort 传送 4 个参数. 第一个参数指向物体数组 (被排序的数组d). 第二个参数是我们想排序数组的个数... 当然,我们想所有的排序的物体普遍的被显示(各个level). 第三个参数规定物体结构的大不, 第四个参数指向我们的 Compare() 函数.

大概有更好的排序结构的方法, 但是 qsort() 工作起来... 快速方便,简单易用!

这个是重要的知识点, 若你们想用 glAlphaFunc() 和 glEnable(GL_ALPHA_TEST), 排序是没必要的. 然而, 用Alpha 功能你被限制在完全透明或完全白底混合, 没有中间值. 用 Blendfunc()排序用一些更多的工作,但他顾及半透明物体.

  
   

// 排序物体从距离:我们物体数组的开始地址 *** MSDN 代码修改为这个 TUT ***// 各种的数按// 各自的要素的// 指针比较的函数
 qsort((void *) &object, level, sizeof(struct objects), (compfn)Compare );
}

   
初始化的代码总是一样的. 首先的现两行取得我们window 的消息和我们建盘消息. 然后我们用 srand() 建一个基于时间的多样化的游戏. 之后我们调入 TGA 图片并用LoadTGA()转换到材质 . 先前的 5个图片是将穿过屏幕的物体. Explode 是我们爆炸动画, 大地和天空 弥补现场背景, crosshair是你在屏幕上看到表现鼠标当前位置的十字光标, 最后, 用来显示分数,标题和士气值的字体的图片. 若任何调入图片的失误,则到返回 FALSE 值, 并程序结束. 值得注意的是这些基本代码不是返回整数型(INIT)的 FAILED 错误消息.
  
   

BOOL Initialize (GL_Window* window, Keys* keys)    // 任何 OpenGL 从这初始化
{
 g_window = window;
 g_keys  = keys;

 srand( (unsigned)time( NULL ) );     // 使随机化事件

 if ((!LoadTGA(&textures[0],"Data/BlueFace.tga")) ||   // 调入蓝面材质
  (!LoadTGA(&textures[1],"Data/Bucket.tga")) ||   // 调入水桶材质
  (!LoadTGA(&textures[2],"Data/Target.tga")) ||   // 调入靶子材质
  (!LoadTGA(&textures[3],"Data/Coke.tga")) ||   // 调入 可乐材质
  (!LoadTGA(&textures[4],"Data/Vase.tga")) ||   // 调入 花瓶 材质
  (!LoadTGA(&textures[5],"Data/Explode.tga")) ||   // 调入 爆炸材质
  (!LoadTGA(&textures[6],"Data/Ground.tga")) ||   // 调入 地面 材质
  (!LoadTGA(&textures[7],"Data/Sky.tga")) ||   // 调入 天空 材质
  (!LoadTGA(&textures[8],"Data/Crosshair.tga")) ||  // 调入 十字光标 材质
  (!LoadTGA(&textures[9],"Data/Font.tga")))   // 调入 字符 材质
 {
  return FALSE;      // 若调入失败, 返回错误
 }

   
若所有图片调入成功则轮到材质, 我们能继续初始化. 字体材质被调入, 因而保险能建立我们的字体. 我们跳入BuildFont()来做这些.
然后我们设置OpenGL. 背景色为黑, alpha 也设为0.0f. 深度缓冲区设为激活小于或等于测试.

glBlendFunc() 是很重要的一行代码. 我们设混合函数(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA). 这些加上alpha变量的屏幕上的混合物体存在物体的材质. 在设置混合模式之后, 我们激活blending(混合). 然后我们打开 2D 材质贴图, 最后,打开 GL_CULL_FACE. 这是去除每个物体的后面( 没有一点浪费在一些我们看不到的循环 ). 画一些四边形逆时针卷动 ,因而精致而适当的面片.

早先的教程我谈论使用glAlphaFunc()代替alpha 混合. 若你想用Alpha 函数, 注释出的两行混合代码和不注释的两行在glEnable(GL_BLEND)之下. 你也能注释出qsort()函数在 InitObject() 部分里的代码.

程序应该运行ok,但sky 材质将不在这. 因为sky的材质已是一个alpha 变量0.5f. 当早在我说关于Alpha函数, 我提及它只工作在alpha 变量0 或 1. 若你想它出现,你将不得不修改sky的材质alpha 通道! 再则, 若你决定用Alpha 函数代替, 你不得排序物体.两个方法都有好处! 再下而是从SGI 网站的快速引用:

"alpha 函数丢弃细节,代替画他们在结构缓冲器里. 因此排序原来的物体不是必须的 (除了一些其它像混合alpha模式是打开的). 不占优势的是象素必须完全白底或完全透明".

  
   

 BuildFont();        // 建立我们的字体显示列表

 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);     // 黑色背景
 glClearDepth(1.0f);       // 安装深度缓冲器
 glDepthFunc(GL_LEQUAL);       // 深度的类型测试
 glEnable(GL_DEPTH_TEST);       // 打开深度测试
 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);    // 打开 Alpha 混合
 glEnable(GL_BLEND);       // 打开混合
 glAlphaFunc(GL_GREATER,0.1f);      // 设 Alpha 测试
 glEnable(GL_ALPHA_TEST);       // 打开 Alpha 测试
 glEnable(GL_TEXTURE_2D);       // 打开材质贴图
 glEnable(GL_CULL_FACE);       // 去掉画物体的背面

   
在程序的这段, 还没有物体被定义,所以循环30个物体,每个物体都调InitObject().  
   

 for (int loop=0; loop<30; loop++)     // 循环在 30 个物体Objects
  InitObject(loop);      // 初始化每个物体

 return TRUE;       // 返回正确 (设初值成功)
}

   
在初始化代码里, 调入BuildFont() 建立95 的显示列表. 所以这里在程序结束前删掉95显示列表  
   

void Deinitialize (void)       // 任何user 结束初始化从这
{
 glDeleteLists(base,95);       // 删掉所有95 字体显示列表
}

   
现在为急速原始物体... 是实际被选?形锾宓拇?. 第一行为我们选择物体的信息分配内存. hits 是当选择时碰撞迅检测的次数.
  
   

void Selection(void)        // 这是选择正确
{
 GLuint buffer[512];       //  设选择缓冲
 GLint hits;        // 选择物体的数

   
下面的代码, 若游戏结束(FALSE).没有选任何, 返回(exit). 若游戏还在运行 (TRUE),用Playsound() 命令放射击的声间. 仅仅调Selection()的时间是在当已鼠标按下时和每次按下按键调用时, 想放射击的声音. 声音在放在 async 模式 ,所以放音乐是程序不会停.   
   

 if (game)        // 游戏是否结束?
  return;       // 是,返回, 不在检测 Hits
 
 PlaySound("data/shot.wav",NULL,SND_ASYNC);    // 放音乐 Gun Shot

   
设视点. viewport[] 包括当前 x, y, 当前的视点(OpenGL Window)长度,宽度.

glGetIntegerv(GL_VIEWPORT, viewport) 取当前视点存在viewport[]. 最初,等于 OpenGL 窗口维数. glSelectBuffer(512, buffer) 说 OpenGL 用这个内存.
  
   


 // 视点的大小. [0] 是 <x>, [1] 是 <y>, [2] 是 <length>, [3] 是 <width>
 GLint viewport[4];

 // 这是设视点的数组在屏幕窗口的位置
 glGetIntegerv(GL_VIEWPORT, viewport);
 glSelectBuffer(512, buffer);      // 告诉 OpenGL 使我们的数组来选择

   
存opengl的模式. 在这个模式什么也不画. 代替, 在选择模式物体渲染信息存在缓存.
下面初实化name 堆栈,通过调入glInitNames() 和glPushName(0). I它重要的是标记若程序不在选择模式, 一个到glPushName()调用将忽略. 当然在选择的模试, 但这一些是是紧记的.

  
   


 // 设 OpenGL 选择模式. 将不画东西. 物体 ID’的广度放在内存
 (void) glRenderMode(GL_SELECT);

 glInitNames();        // 设名字堆栈
 glPushName(0);        // Push 0 (最少一个) 在栈上

   
之后, 不得不限制在光标的下面画图. 为了做这些得用到投影矩阵. 然后把它推到堆栈中.重设矩阵则用到 glLoadIdentity().
用gluPickMatrix()限制的画. 第1个参数是当前鼠标的 x-座标, 第2个参数是当前鼠标的 y-座标, 然后宽和高的选区. 最后当前的 viewport[]. viewport[] 是指出视点的边界. x 和_y 将在选区的中心.

  
   

 glMatrixMode(GL_PROJECTION);      // 选投影矩阵
 glPushMatrix();        // 压入投影矩阵
 glLoadIdentity();        // 重设矩阵

 //  这是建一个矩阵使鼠标在屏幕缩放
 gluPickMatrix((GLdouble) mouse_x, (GLdouble) (viewport[3]-mouse_y), 1.0f, 1.0f, viewport);

   
调入 gluPerspective() 应用透视矩阵 ,被gluPickMatrix()选择矩阵限制所画区域 .
打开modelview 矩阵,调用DrawTargets()画我们的靶子. 画靶子在DrawTargets() 而不在 Draw() 是因为仅仅想选择物体的碰撞检测且, 不是天空,大地,光标.

之后, 打开回到发射矩阵, 从堆栈中弹出矩阵. 之扣打开回到modelview 矩阵.

最后一行,回到渲染模式 因而物体画的很真实的在屏幕上. hits 将采集gluPickMatrix()所需要取渲染的物体数 .

  
   

 // 应用透视矩阵
 gluPerspective(45.0f, (GLfloat) (viewport[2]-viewport[0])/(GLfloat) (viewport[3]-viewport[1]), 0.1f, 100.0f);
 glMatrixMode(GL_MODELVIEW);       // 选择模型变换矩阵
 DrawTargets();        // 画目标
 glMatrixMode(GL_PROJECTION);      // 选择投影变换矩阵
 glPopMatrix();        // 取出投影矩阵
 glMatrixMode(GL_MODELVIEW);       // 选模式显示矩阵
 hits=glRenderMode(GL_RENDER);      // 切换模式, 找出有多少

   
检查若多于0 个hits 记录. 若这样, 设choose 为 第一个物体的名子. depth 取得它有多远.
每个hit 分有4 个项目在内存. 第一,在名子堆栈上打击发生时的数字 .第二, 所选物体的最小z值. 第三,所选物体的最大 z 值, 最后,在同一时间里所选物体名子堆栈的内容 (物体的名子). 在这一课,我们仅对最小z值和物体名子感兴趣.

  
   

 if (hits > 0)        // 若有大于0个 Hits
 {
  int choose = buffer[3];     // 选择第一物体
  int depth = buffer[1];      // 存它有多远
   

做循环所有hits 使没有物体在第一个物体旁边. 否则, 两个物体会重叠, 一个物体碰到另一个.当你射击时, 重叠的物体会被误选.  
   

  for (int loop = 1; loop < hits; loop++)    // 循环所有检测到的物体
  {
   // 对于其它的物体
   if (buffer[loop*4+1] < GLuint(depth))
   {
    choose = buffer[loop*4+3];    // 选择更近的物体
    depth = buffer[loop*4+1];    // 保存它有多远
   }
  }

   
若物体被选.
  
   


  if (!object[choose].hit)     // 如果物体还没有被击中
  {
   object[choose].hit=TRUE;    // 标记物体象被击中
   score+=1;      // 增加分数
   kills+=1;      // 加被杀数

   
如下
  
   

   if (kills>level*5)     // 已有新的级?
   {
    miss=0;     // 失掉数回0
    kills=0;     // 设 Kills数为0
    level+=1;     // 加 Level
    if (level>30)    // 高过 30?
     level=30;    // 设 Level 为 30 (你是 God 吗?)
   }
  }
 }
}

   
如下
  
   

void Update(DWORD milliseconds)       // 这里用来更新
{
 if (g_keys->keyDown[VK_ESCAPE])      // 按下 ESC?
 {
  TerminateApplication (g_window);     // 推出程序
 }

   
如下   
   

 if (g_keys->keyDown[' '] && game)     // 按下空格键?
 {
  for (int loop=0; loop<30; loop++)    // 循环所有的物体
   InitObject(loop);     // 初始化

  game=FALSE;      //设game为false
  score=0;       // 分数为0
  level=1;       // 级别为1
  kills=0;       // 杀敌数为0
  miss=0;       // 漏过数为0
 }


 if (g_keys->keyDown[VK_F1])      // 按下f1?
 {
  ToggleFullscreen (g_window);    // 换到全屏模式
 }

 roll-=milliseconds*0.00005f;     // 云的旋转

 for (int loop=0; loop<level; loop++)     // 循环所有的物体
 {

   
下面的代码按物体的运动方向更新所有的运动  
   

  if (object[loop].rot==1)     
   object[loop].spin-=0.2f*(float(loop+milliseconds)); // 若顺时针,则顺时针旋转

  if (object[loop].rot==2)
   object[loop].spin+=0.2f*(float(loop+milliseconds)); // 若逆时针,则逆时针旋转

  if (object[loop].dir==1)     
   object[loop].x+=0.012f*float(milliseconds);  // 向右移动

  if (object[loop].dir==0)     
   object[loop].x-=0.012f*float(milliseconds);  // 向左移动

  if (object[loop].dir==2)     
   object[loop].y+=0.012f*float(milliseconds);  // 向上移动

  if (object[loop].dir==3)     
   object[loop].y-=0.0025f*float(milliseconds);  // 向下移动

   
下面的代码处理当物体移动到边缘处,如果你没有击中它的结果  
   

  // 如果到达左边界,你没有击中,则增加丢失的目标数
  if ((object[loop].x<(object[loop].distance-15.0f)/2.0f) && (object[loop].dir==0) && !object[loop].hit)
  {
   miss+=1;      
   object[loop].hit=TRUE;     
  }


  // 如果到达右边界,你没有击中,则增加丢失的目标数
  if ((object[loop].x>-(object[loop].distance-15.0f)/2.0f) && (object[loop].dir==1) && !object[loop].hit)
  {
   miss+=1;      
   object[loop].hit=TRUE;    
  }


  // 如果到达下边界,你没有击中,则增加丢失的目标数
  if ((object[loop].y<-2.0f) && (object[loop].dir==3) && !object[loop].hit)
  {
   miss+=1;      
   object[loop].hit=TRUE;     
  }

  //如果到达左边界,你没有击中,则方向变为向下
  if ((object[loop].y>4.5f) && (object[loop].dir==2))  
   object[loop].dir=3;    
 }
}

   
下面的代码在屏幕上绘制一个图像  
   

void Object(float width,float height,GLuint texid)    // 画物体用需要的宽,高,材质
{
 glBindTexture(GL_TEXTURE_2D, textures[texid].texID);   // 选合适的材质
 glBegin(GL_QUADS);       // 开始画四边形
  glTexCoord2f(0.0f,0.0f); glVertex3f(-width,-height,0.0f); 
  glTexCoord2f(1.0f,0.0f); glVertex3f( width,-height,0.0f); 
  glTexCoord2f(1.0f,1.0f); glVertex3f( width, height,0.0f); 
  glTexCoord2f(0.0f,1.0f); glVertex3f(-width, height,0.0f); 
 glEnd();        
}

   
下面的代码绘制爆炸的效果  
   

 void Explosion(int num)      // 画爆炸动画的1帧
{
 float ex = (float)((object[num].frame/4)%4)/4.0f;   // 计算爆炸时生成的x的纹理坐标
 float ey = (float)((object[num].frame/4)/4)/4.0f;   // 计算爆炸时生成的y的纹理坐标

 glBindTexture(GL_TEXTURE_2D, textures[5].texID);   // 选择爆炸的纹理
 glBegin(GL_QUADS);       
  glTexCoord2f(ex      ,1.0f-(ey      )); glVertex3f(-1.0f,-1.0f,0.0f); 
  glTexCoord2f(ex+0.25f,1.0f-(ey      )); glVertex3f( 1.0f,-1.0f,0.0f); 
  glTexCoord2f(ex+0.25f,1.0f-(ey+0.25f)); glVertex3f( 1.0f, 1.0f,0.0f);
  glTexCoord2f(ex      ,1.0f-(ey+0.25f)); glVertex3f(-1.0f, 1.0f,0.0f); 
 glEnd();        
   
增加帧数,如果大于63,则重置动画  
   


 object[num].frame+=1;      // 加当前的爆炸动画帧
 if (object[num].frame>63)      // 是否已完成所有的16帧?
 {
  InitObject(num);      // 定义物体 (给新的变量)
 }
}

   
画靶子  
   

void DrawTargets(void)        // 画靶子
{
 glLoadIdentity();       
 glTranslatef(0.0f,0.0f,-10.0f);      // 移入屏幕 20 个单位
 for (int loop=0; loop<level; loop++)      // 循环在 9 个物体
 {

  glLoadName(loop);       // 给物体新名字
  glPushMatrix();       // 存矩阵
  glTranslatef(object[loop].x,object[loop].y,object[loop].distance); // 物体的位置 (x,y)

  if (object[loop].hit)      // 若物体已被点击
  {
   Explosion(loop);      // 画爆炸动画
  }
  else        
  {
   glRotatef(object[loop].spin,0.0f,0.0f,1.0f);   // 旋转物体
   Object(size[object[loop].texid].w,size[object[loop].texid].h,object[loop].texid); // 画物体
  }
  glPopMatrix();       // 弹出矩阵
 }
}

   
下面的代码绘制整个场景  
   

void Draw(void)        // 画我们的现场
{
 glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // 清除屏幕和深度缓冲
 glLoadIdentity();       // 重设矩阵

   
下面的代码绘制飘动的天空,它由四块纹理组成,每一块的移动速度都不一样,并把它们混合起来  
   

 glPushMatrix();        
 glBindTexture(GL_TEXTURE_2D, textures[7].texID);   // 选天空的材质
 glBegin(GL_QUADS);      
  glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f); 
  glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f); 
  glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f); 
  glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f); 

  glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f);  
  glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f);  
  glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f);  
  glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f);  
  glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,0.0f); 
  glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,0.0f); 
  glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f); 
  glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f); 

  glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,0.0f);  
  glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,0.0f);  
  glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f);  
  glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f);  
 glEnd();       
   
下面的代码绘制地面  
   

 glBindTexture(GL_TEXTURE_2D, textures[6].texID);   // 大地材质
 glBegin(GL_QUADS);       
  glTexCoord2f(7.0f,4.0f-roll); glVertex3f( 27.0f,-3.0f,-50.0f); 
  glTexCoord2f(0.0f,4.0f-roll); glVertex3f(-27.0f,-3.0f,-50.0f); 
  glTexCoord2f(0.0f,0.0f-roll); glVertex3f(-27.0f,-3.0f,0.0f); 
  glTexCoord2f(7.0f,0.0f-roll); glVertex3f( 27.0f,-3.0f,0.0f); 
 glEnd();        
   
绘制我们的靶子  
   


 DrawTargets();        // 画我们的靶子
 glPopMatrix();        
   
下面的代码绘制我们的十字光标
  
   


 // 十字光标 (在光标里)
 RECT window;        // 用来存窗口位置
 GetClientRect (g_window->hWnd,&window);     // 取窗口位置
 glMatrixMode(GL_PROJECTION);      
 glPushMatrix();        
 glLoadIdentity();      
 glOrtho(0,window.right,0,window.bottom,-1,1);    // 设置为正投影
 glMatrixMode(GL_MODELVIEW);      
 glTranslated(mouse_x,window.bottom-mouse_y,0.0f);   // 移动到当前鼠标位置
 Object(16,16,8);       // 画十字光标

   
下面的代码用来显示帮助文字  
   


 // 游戏状态 / 标题名称
 glPrint(240,450,"NeHe Productions");     // 输出 标题名称
 glPrint(10,10,"Level: %i",level);     // 输出 等级
 glPrint(250,10,"Score: %i",score);     // 输出 分数

   
如果丢失10个物体,游戏结束  
   

 if (miss>9)        // 我们已丢失 10 个物体?
 {
  miss=9;        // 限制丢失是10个
  game=TRUE;       // 游戏结束
 }

   
在下面的代码里, 我们查看若game 是TRUE. 若 game 是TRUE, 我们输出 ’GAME OVER’游戏结束的消息. 若game 是false, 我们输出 玩家的士气morale (到10溢出). 士气morale是被设计用来从10减去玩家失误的次数(miss) . 玩家失掉的越多, 士气越低.
  
   

 if (game)        // 游戏是否结束?
  glPrint(490,10,"GAME OVER");    // 结束消息
 else
  glPrint(490,10,"Morale: %i/10",10-miss);   // 输出剩余生命
   
最后做的事我们选投影矩阵, 恢复(取出) 我们的矩阵返回到前一个情形, 设矩阵模式为 modelview ,刷新缓冲区 ,使所有物体被渲染.   
   

 glMatrixMode(GL_PROJECTION);      
 glPopMatrix();        
 glMatrixMode(GL_MODELVIEW);      

 glFlush();        
}

   
这课程是多次熬夜的成果, 许多的时间用来编码和写 HTML. 在这一课结束的时候你应你会学会怎样picking, sorting, alpha blending and alpha testing 工作. 制做点和软件. 每一个游戏, 到精选的GUI’们.最好的未来是制做时你不用记录物体. 你给一个名字和碰撞 . 这很简单! 用alpha 通道和alpha 测试你能使物体完全显示, 或漏出一些. 结果是很好, 你不用担心关于显示物体的材质, 除非你不显示他们! 同以往一样, 我希望你喜欢这个课程,愿看到一些好的游戏或好的项目从这个课程诞生.如果你有什么问题或找到错误,让我知道 ... 我仅是一个普通人 :)
我将花大量的时间加入东西像物理系统, 更多图, 更多声音, 等. 虽然只是一个课程! 我不写不按车灯和车轮. 我写这个用尽量不混乱的方法教你 OpenGL . 我希望看到一些严谨的修改. 若你找一些cool的课程发给我一份. 若是好的修改我将放到下载页. 若有足够充分的修改我会专注修改这个课程的版本! 我在这里给你一个起点. 剩下的靠你了 :)

要点: 这是很重要的,称为glTexImage2D 你设为两种格式国际 GL_RGBA. 否则 alpha blending 将不工作!




W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
695.313ms