当然,刚才说过我们是要绘制一个立方盒,我当然没有忘记,只是在这之前想让更多的读者熟悉一下OpenGL的绘图机制。其它方盒很简单,我们应该有这样一组数据,并把它做为全局变量。每个顶点用X、Y、Z三维short值来表示,一个八个顶点,你可以跟据这些数据在纸上手画一个正方体出来吗?
short nSrcBox[ 3 * 8 ] = { 5, 5, 0, 5, 5, 10, 5,-5, 0, 5, -5, 10, -5,-5,0, -5, -5, 10, -5, 5, 0, -5, 5, 10, };
在绘制时我们可以一个三角形一个三角形的画,也可以像上一个例子一样,一个四边形一个四边形的画,但为了后面我们要讲述的一些东东,先看三角形画吧。每一个面由两个三角形组成,一共六个面,十二个三角形。如果给上面的八个顶点编号为0-7,那么我们可以按照这样的顺序来画:
0, 4, 6, 0, 2, 4, 0, 6, 7, 0, 7, 1, 0, 3, 2, 0, 1, 3, 5, 2, 3, 5, 4, 2, 5, 6, 4, 5, 7, 6, 5, 1, 7, 5, 3, 1,
这里需要注意的是右手定则,在OpenGL中按照绘制一个三角形三维顶点的顺序,用右手正正握这个三角形,大姆指竖起的方向就是这个面的正方向。其实你已经在不知不觉中了解了什么是索引数组,就是上面那一堆数字,我们把他们保存起来,做为全局变量:
BYTE byIndex[36] ={ 0, 4, 6, 0, 2, 4, 0, 6, 7, 0, 7, 1, 0, 3, 2, 0, 1, 3, 5, 2, 3, 5, 4, 2, 5, 6, 4, 5, 7, 6, 5, 1, 7, 5, 3, 1, };
好了,现在是万事具体只欠东风了,怎么画呢?也许你会想到和上面的程序一样调用一个个的glVertex来绘制这些顶点,是的,你是正确的,但这样太慢而且太复杂了,算算你一共要写多少个glVertex呢?幸好OpenGL为我们提供了一个方便绘图机制:数组绘制。在初始化代码的最下面加上这样的语句来指定顶点数组:
glEnableClientState( GL_VERTEX_ARRAY ); // 启用顶点数组 glVertexPointer( 3, GL_SHORT, 0, nSrcBox ); // 设置顶点数组地址 |
然后在OnDraw的Clear和Swap之间加上这一句话就足够了:
// 后两个参数指定索引数组值的类型和数组地址 glDrawElements( GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, byIndex ); |
你应该可以看到一个旋转的立方体了,但它仍然是混沌一片,看不清棱角,这是由于你没有指定光线反射的方向,下面我们将再引入一个概念“法向量”。在现实生活中,人眼看到物体是由于物体对光线的反射,如果物体不反射光,或着不向人的眼睛方向反射,人眼将认为这个物体是没有光照的。先不说漫反射,每一个镜面反射的物体的入射角和射出角都是相等的,因而镜面反射物体的法向量因该是垂直于平面的。由于光的粒子性,漫反射物体我们可以认为是由无限多个镜面反射物体所组成的一种复杂物体。根据这些理论,OpenGL在绘制三维物体时将按照给定的顶点法向量值来计算这个面的明暗亮度。计算法向量要用到一个数学基础知识,可能对于一些人来说有些晦涩难懂,不过没有关系,这里有现成的函数,你直接调用就可以计算了:
// 归一化函数,没什么可说的 template <typename Type> HRESULT ReduceToUnit( Type *pVector ) { double dLength = sqrt( (double)pVector[0] * (double)pVector[0] + (double)pVector[1] * (double)pVector[1] + (double)pVector[2] * (double)pVector[2] );
if ( FLOATEQUAL( dLength, 0.0, 1e-8 ) ) { dLength = 1.0; } pVector[0] /= (Type)dLength; pVector[1] /= (Type)dLength; pVector[2] /= (Type)dLength; return S_OK; }
// 第一个参数用9个double表示一个三角型,三角型的法向量将由第二个参数传出。 template <typename T1, typename T2> HRESULT CalcNormal( const T1 *pVertical, T2 *pNormal ) { T1 d1[3], d2[3]; d1[0] = pVertical[0] - pVertical[3]; d1[1] = pVertical[1] - pVertical[4]; d1[2] = pVertical[2] - pVertical[5]; d2[0] = pVertical[3] - pVertical[6]; d2[1] = pVertical[4] - pVertical[7]; d2[2] = pVertical[5] - pVertical[8]; pNormal[0] = (T2)( d1[1] * d2[2] - d1[2] * d2[1] ); pNormal[1] = (T2)( d1[2] * d2[0] - d1[0] * d2[2] ); pNormal[2] = (T2)( d1[0] * d2[1] - d1[1] * d2[0] ); return ReduceToUnit( pNormal ); } |
问题又出现了,一个顶点可以属于不同的面,但是法向量确只有一个,给个顶点赋任何一个面的法向量都将导致显示不正常,那么我们该怎么办呢?只好按照索引将这些顶点“拆开”。
for ( int i = 0; i < 36; i++ ) { // nTempBox是一个临时数组变量,用于保存拆开后的数据。 MoveMemory( &nTempBox[ i * 3 ], &nSrcBox[ byIndex[i] * 3 ], sizeof(nTempBox[0]) * 3 ); } |
下面我们就可以计算法向量了。
for ( int i = 0; i < 12; i++ ) { // 跟据一个三角形的三个顶点计算出该三角形的法向量 CalcNormal( &nTempBox[ i * 9 ], &fNormal[ i * 9 ] ); // 并把这个法向量赋给这个三角形的三个顶点 MoveMemory( &fNormal[ i * 9 + 3 ], &fNormal[ i * 9 ], sizeof(fNormal[0]) * 3 ); MoveMemory( &fNormal[ i * 9 + 6 ], &fNormal[ i * 9 ], sizeof(fNormal[0]) * 3 ); } glEnableClientState( GL_VERTEX_ARRAY ); glVertexPointer( 3, GL_SHORT, 0, nTempBox ); glEnableClientState( GL_NORMAL_ARRAY ); // 启用法向量 glNormalPointer( GL_FLOAT, 0, fNormal ); // 填写法向量数组地址 |
因为用不着索引数组,下面的绘制代码将更加简单:
| glDrawArrays( GL_TRIANGLES, 0, 36 ); |
|