用OpenInventor实现的NeHe OpenGL教程-第十课
这节课我们将加载一个3D场景,并在这个场景中做自由漫游。读者将看到加载3D场景不是一件很难的事情,难点是3D场景的数据组织。不过这些是美工的事情,和我们程序员关系不大
^_^ 。
在程序开始的部分我们新定义了一些变量,这些变量的作用和NeHe教程中定义的变量有相同的作用。
SoTexture2* g_pTexture = NULL; //
场景的纹理对象
SoComplexity* g_pComplexity = NULL;//
定义纹理的精度
SoTranslation* g_pPosTrans = NULL; //
定义当前位置
SoRotation* g_pLookupdownRotation = NULL;//
定义当前视线的上下方向
SoRotation* g_pScenerotyRotation = NULL;//
定义当前视线的方向
下面这些变量和NeHe教程中定义的同名变量含义相同。请读者参考NeHe教程
const
float piover180 = 0.0174532925f; //
角度和弧度之间转换因子
float
heading = 0.0f;
float
xpos = 0.0f;
float
zpos = 0.0f;
float
yrot = 0.0f;
float
walkbias = 0;
float
walkbiasangle = 0;
float
lookupdown = 0.0f;
bool
bBlend = false;
定义一个顶点数据结构
typedef
struct tagVERTEX
{
float x,y,z; //
顶点的位置
float u,v; //
顶点的纹理坐标
}VERTEX;
定义一个三角面数据结构,每个三角面有三个顶点构成的
typedef
struct tagTRIANGLE
{
VERTEX vertex[3];
}TRIANGLE;
定义整个场景包含的所有三角面
typedef
struct tagSECTOR
{
int iNumTriangle; //
三角面的个数
TRIANGLE *pTriangle;//
三角面的数据
}SECTOR;
从数据文件中读一行字符串
void
readstr(FILE *f,char *string)
{
do
{
fgets(string, 255, f);
} while ((string[0] == '/') || (string[0] == '/n'));
}
下面的函数将从world.txt数据文件中读入场景数据,数据保存在Sector变量中,场景数据包括每个顶点的位置坐标和纹理坐标。
SetupWorld函数已经在NeHe教程中做了很详细的解释,这里就不在重复了。至于world.txt文件是如何得到的,我想只有NeHe自己知道,可能是用专门的模型建模软件生成的。
void
SetupWorld()
{
………………
.
}
在函数BuildScene中,我们编写如下的代码。
void
BuildScene(void)
{
首先加载场景数据
SetupWorld();
向场景中增加一个SoCallback节点,这个节点的作用是可以在它的回调函数中调用OpenGL函数。这个节点的作用我们在本教程的第九课已经做了介绍。
SoCallback *pGlCallback = new SoCallback();
pGlCallback->setCallback(GlCB, NULL); //
设置用户自定义回调函数GLCB
g_pOivSceneRoot->addChild(pGlCallback);
设置光照模型为BASE_COLOR
SoLightModel *pSoLightModel = new SoLightModel;
pSoLightModel->model = SoLazyElement::BASE_COLOR;
g_pOivSceneRoot->addChild(pSoLightModel);
增加上控制视线上下方向的旋转节点
g_pLookupdownRotation = new SoRotation;
g_pLookupdownRotation->rotation.setValue(SbVec3f(1.0f,0,0),0);
g_pOivSceneRoot->addChild(g_pLookupdownRotation);
增加上控制视线上下方向的旋转节点
g_pScenerotyRotation = new SoRotation;
g_pScenerotyRotation->rotation.setValue(SbVec3f(0,1.0f,0),3.1415 * 2);
g_pOivSceneRoot->addChild(g_pScenerotyRotation);
增加上控制当前漫游位置的平移节点
g_pPosTrans = new SoTranslation;
xpos -= (float)sin(heading * piover180) * 0.05f;
zpos -= (float)cos(heading * piover180) * 0.05f;
if (walkbiasangle >= 359.0f)
walkbiasangle = 0.0f;
else
walkbiasangle += 10;
walkbias = (float)sin(walkbiasangle * piover180) / 20.0f;
g_pPosTrans->translation.setValue(-xpos,-walkbias - 0.25f,-zpos);
g_pOivSceneRoot->addChild(g_pPosTrans);
加载纹理数据
g_pComplexity = new SoComplexity;
g_pComplexity->textureQuality = 0.1;
g_pOivSceneRoot->addChild(g_pComplexity);
g_pTexture = new SoTexture2;
g_pTexture->filename.setValue("../Data/Mud.png");
g_pOivSceneRoot->addChild(g_pTexture);
下面将增加场景数据。我们从world.txt数据文件中读取的数据,不适合直接在OpenInventor中使用,必须做一下转换。OpenInventor的顶点数据和纹理坐标数据是分离的,是保存在不同的节点中,而我们从world.txt中读取的数据,顶点和纹理数据是放在一个结构体中。所以我们要做如下的转换。
int32_t *pNumVertices = new int32_t[Sector.iNumTriangle]; //
保存文件面的顶点数
SbVec3f *pVertices = new SbVec3f[Sector.iNumTriangle * 3];//
保存所有顶点坐标数据
SbVec2f *pTextureCoord = new SbVec2f[Sector.iNumTriangle * 3];//
保存所有顶点纹理坐标数据
for
(int i = 0; i < Sector.iNumTriangle; i++)
{
pVertices[i * 3].setValue( Sector.pTriangle[i].vertex[0].x,
Sector.pTriangle[i].vertex[0].y,
Sector.pTriangle[i].vertex[0].z);
pVertices[i * 3 + 1].setValue( Sector.pTriangle[i].vertex[1].x,
Sector.pTriangle[i].vertex[1].y,
Sector.pTriangle[i].vertex[1].z);
pVertices[i * 3 + 2].setValue( Sector.pTriangle[i].vertex[2].x,
Sector.pTriangle[i].vertex[2].y,
Sector.pTriangle[i].vertex[2].z);
pTextureCoord[i * 3].setValue( Sector.pTriangle[i].vertex[0].u,
Sector.pTriangle[i].vertex[0].v);
pTextureCoord[i * 3 + 1].setValue( Sector.pTriangle[i].vertex[1].u,
Sector.pTriangle[i].vertex[1].v);
pTextureCoord[i * 3 + 2].setValue( Sector.pTriangle[i].vertex[2].u,
Sector.pTriangle[i].vertex[2].v);
pNumVertices[i] = 3;
}
增加上保存顶点的节点,用来指定所有顶点数据
SoCoordinate3 *Coords = new SoCoordinate3;
Coords->point.setValues(0,Sector.iNumTriangle * 3,pVertices);
g_pOivSceneRoot->addChild(Coords);
delete []pVertices; //
数据已经保存在OpenInventor内部了,所以我们自己申请的内存要释放掉。
增加上保存顶点纹理坐标的节点
SoTextureCoordinate2 *texCoord = new SoTextureCoordinate2;
texCoord->point.setValues(0,Sector.iNumTriangle * 3,pTextureCoord);
g_pOivSceneRoot->addChild(texCoord);
delete []pTextureCoord;
定义每个平面
SoFaceSet *FaceSet = new SoFaceSet;
FaceSet->numVertices.setValues(0, Sector.iNumTriangle, pNumVertices);
g_pOivSceneRoot->addChild(FaceSet);
delete []pNumVertices;
定义键盘回调节点,响应用户按键事件
SoEventCallback* pEventCallback = new SoEventCallback;
pEventCallback->addEventCallback(SoKeyboardEvent::getClassTypeId(),KeyboardEventCB,g_pOivSceneRoot);
g_pOivSceneRoot->addChild(pEventCallback);
}
下面我们编写SoCallback节点的响应函数,在OpenInventor遍历到SoCallback节点时会调用这个函数。在这个函数中,我们可以调用OpenGL命令,因为这时OpenGL Context是合法的。
void
GlCB(void *data, SoAction *action)
{
if (action->isOfType(SoGLRenderAction::getClassTypeId()))
{
if(bBlend)
{
glBlendFunc(GL_SRC_ALPHA,GL_ONE);//
定义混合计算公式
glEnable(GL_BLEND); //
启动混合运算
glDisable(GL_DEPTH_TEST); //
禁止深度检测
}
else
{
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
}
}
下面是键盘响应函数,我们在这个函数中响应上下箭头按键,Page_Up/Page_Down按键,其中计算当前位置和方向的公式和NeHe完全一样,读者可以参考NeHe教程,这里就不详细介绍了。请注意,OpenInventor角度的单位为弧度
void
KeyboardEventCB(void *userData, SoEventCallback *pEventCB)
{
…………………
}
现在编译运行我们程序,屏幕上会显示一个房间。读者可以按下前后箭头键来前后运动,按下左右方向键可以转动当前视线方向,按下PnUp/PnDn键来改变当前视线的上下方向。效果类似我们玩的很多第一人称游戏。按下F键可以改变纹理的品质。按下B键场景将透明显示。所有的效果和NeHe第十课是相同的。
有一些补充需要说明一下,读者测试本课程的例子程序时,可能会感觉我们的程序没有NeHe的程序的运动速度快。这是因为我们是在响应键盘的地方更新场景,而我们知道,当按下键盘某个按键后,键盘硬件会持续发送按键消息,但这些消息之间是有时间间隔的。NeHe框架中采用的是在消息空闲的时候更新场景,所以它的更新频率要远高于我们,当然运动速度就要快些。但这样做的代价也是很大的,可能会导致CPU总是100%.
当我们在场景中漫游时,如果向前走,NeHe采用的方法是让场景中所有物体都沿着反方向运动,也就是说我们自己不动,场景中的物体在以反方向运动。这样做虽然可以达到漫游的目的,但和我们主观感觉不同。在OpenInventor中,我们可以得到当前的照相机对象,我们可以控制照相机的位置和方向,这种方式和我们在现实中观察物体的方式是一样的,比较符合我们的思维,代码也更加容易理解。读者如果感兴趣的话,可以试试用这种方式实现场景漫游。
本课的完整代码
下载。 (VC 2003 + Coin2.5)
后记
OpenInventor是一种基于OpenGL的面向对象的三维图形软件开发包。使用这个开发包,程序员可以快速、简洁地开发出各种类型的交互式三维图形软件。这里不对OpenInventor做详细的介绍,读者如果感兴趣,可以阅读我的blog中的这篇文章《 OpenInventor
简介》。
NeHe教程是目前针对初学者来说最好的OpenGL教程,它可以带领读者由浅入深,循序渐进地掌握OpenGL编程技巧。到目前为止(2007年11月),NeHe教程一共有48节。我的计划是使用OpenInventor来实现所有48节课程同样的效果。目的是复习和巩固OpenGL的知识,同时与各位读者交流OpenInventor的使用技巧。
因为篇幅的限制,我不会介绍NeHe教程中OpenGL的实现过程,因为NeHe的教程已经讲解的很清楚了,目前网络中也有NeHe的中文版本。我将使用VC 2003作为主要的编译器。程序框架采用和NeHe一样的Win32程序框架,不使用MFC。程序也可以在VC Express,VC 2005/2008中编译。我采用的OpenInventor开发环境是Coin,这是一个免费开源的OpenInventor开发库。文章 《 OpenInventor
-Coin3D开发环境》 介绍了如何在VC中使用Coin。我使用的Coin版本是2.5。读者可以到 www.coin3d.org 中免费下载。
读者可以在遵循GNU协议的条件下自由使用、修改本文的代码。水平的原因,代码可能不是最优化的,我随时期待读者的指正和交流。转载请注明。谢谢。
我的联系方式:
Blog: < http://blog.csdn.net/RobinHao >
Site: < http://www.openinventor.cn >