用OpenInventor实现的NeHe OpenGL教程-第十九课
这节课我们将讨论怎样使用OpenInventor实现一个简单的粒子系统。我们在NeHe教程中可以看到,一个简单的粒子系统实现起来不像想象中那样的困难。只要能计算好一个粒子的当前状态(速度,加速度,颜色),就很容易完成一个粒子系统的效果。
NeHe教程中使用三角形带来显示每个粒子(GL_TRIANGLE_STRIP),OpenInventor提供了一个相对应的节点类SoTriangleStripSet。SoTriangleStripSet节点是OpenInventor中显示速度最快的节点,特别适合显示大量粒子效果。
下面的代码首先定义一些全局变量。
SoMaterial* g_pParticleColor = NULL; //所有粒子的颜色
SoCoordinate3* g_pParticleCoord = NULL; //所有粒子的位置坐标
SoTextureCoordinate2* g_pParticleTexCoord = NULL; //所有粒子的材质坐标
SoTriangleStripSet* g_pParticleStripSet = NULL; //所有粒子的三角形面集
下面定义的变量和NeHe教程定义的变量名称和含义完全相同,不在赘述
#define MAX_PARTICLES 1000 // Number Of Particles To Create
bool rainbow = true; // Rainbow Mode?
float slowdown = 2.0f; // Slow Down Particles
float xspeed = 0; // Base X Speed (To Allow Keyboard Direction Of Tail)
float yspeed = 0; // Base Y Speed (To Allow Keyboard Direction Of Tail)
float zoom = -40.0f; // Used To Zoom Out
int col; // Current Color Selection
int delay; // Rainbow Effect Delay
typedef struct // Create A Structure For Particle
{
bool active; // Active (Yes/No)
float life; // Particle Life
float fade; // Fade Speed
float r; // Red Value
float g; // Green Value
float b; // Blue Value
float x; // X Position
float y; // Y Position
float z; // Z Position
float xi; // X Direction
float yi; // Y Direction
float zi; // Z Direction
float xg; // X Gravity
float yg; // Y Gravity
float zg; // Z Gravity
} particles; // Particles Structure
particles particle[MAX_PARTICLES]; // Particle Array (Room For Particle Info)
static float colors[12][3] = // Rainbow Of Colors
{
{1.0f,0.5f,0.5f},{1.0f,0.75f,0.5f},{1.0f,1.0f,0.5f},{0.75f,1.0f,0.5f},
{0.5f,1.0f,0.5f},{0.5f,1.0f,0.75f},{0.5f,1.0f,1.0f},{0.5f,0.75f,1.0f},
{0.5f,0.5f,1.0f},{0.75f,0.5f,1.0f},{1.0f,0.5f,1.0f},{1.0f,0.5f,0.75f}
};
float ParticleColorData[MAX_PARTICLES][3];
float ParticleLifeData[MAX_PARTICLES];
SbVec3f ParticleCoordData[MAX_PARTICLES * 4];
SbVec2f ParticleTexCoordData[MAX_PARTICLES * 4];
int ParticleNumVertices[MAX_PARTICLES];
下面的函数用来更新粒子系统中每个粒子的颜色,位置,材质等信息。计算方法和NeHe教程相同。
void UpdateParticle(void)
{
for (int i = 0; i < MAX_PARTICLES; i++)
{
if (particle[i].active) // If The Particle Is Active
{
float x = particle[i].x; // Grab Our Particle X Position
float y = particle[i].y; // Grab Our Particle Y Position
float z = particle[i].z + zoom; // Particle Z Pos + Zoom
ParticleCoordData[i * 4 + 0].setValue(x + 0.5f,y + 0.5f,z);
ParticleCoordData[i * 4 + 1].setValue(x - 0.5f,y + 0.5f,z);
ParticleCoordData[i * 4 + 2].setValue(x + 0.5f,y - 0.5f,z);
ParticleCoordData[i * 4 + 3].setValue(x - 0.5f,y - 0.5f,z);
ParticleTexCoordData[i * 4 + 0].setValue(1,1);
ParticleTexCoordData[i * 4 + 1].setValue(0,1);
ParticleTexCoordData[i * 4 + 2].setValue(1,0);
ParticleTexCoordData[i * 4 + 3].setValue(0,0);
ParticleColorData[i][0] = particle[i].r;
ParticleColorData[i][1] = particle[i].g;
ParticleColorData[i][2] = particle[i].b;
ParticleLifeData[i] = 1 - particle[i].life;
particle[i].x += particle[i].xi / (slowdown * 1000);// Move On The X Axis By X Speed
particle[i].y += particle[i].yi / (slowdown * 1000);// Move On The Y Axis By Y Speed
particle[i].z += particle[i].zi / (slowdown * 1000);// Move On The Z Axis By Z Speed
particle[i].xi += particle[i].xg; // Take Pull On X Axis Into Account
particle[i].yi += particle[i].yg; // Take Pull On Y Axis Into Account
particle[i].zi += particle[i].zg; // Take Pull On Z Axis Into Account
particle[i].life -= particle[i].fade; // Reduce Particles Life By 'Fade'
if (particle[i].life < 0.0f) // If Particle Is Burned Out
{
particle[i].life = 1.0f; // Give It New Life
particle[i].fade = float(rand() % 100) / 1000.0f + 0.003f; // Random Fade Value
particle[i].x = 0.0f; // Center On X Axis
particle[i].y = 0.0f; // Center On Y Axis
particle[i].z = 0.0f; // Center On Z Axis
particle[i].xi = xspeed + float((rand() % 60) - 32.0f); // X Axis Speed And Direction
particle[i].yi = yspeed + float((rand() % 60) - 30.0f); // Y Axis Speed And Direction
particle[i].zi = float((rand() % 60) - 30.0f); // Z Axis Speed And Direction
particle[i].r = colors[col][0]; // Select Red From Color Table
particle[i].g = colors[col][1]; // Select Green From Color Table
particle[i].b = colors[col][2]; // Select Blue From Color Table
}
}
}
g_pParticleColor->diffuseColor.setValuesPointer(MAX_PARTICLES,(float *)ParticleColorData);
g_pParticleColor->transparency.setValuesPointer(MAX_PARTICLES,ParticleLifeData);
g_pParticleCoord->point.setValuesPointer(MAX_PARTICLES * 4,ParticleCoordData);
g_pParticleTexCoord->point.setValuesPointer(MAX_PARTICLES * 4,ParticleTexCoordData);
}
定时回调函数,用来不断更新粒子系统中每个粒子的颜色,位置等状态
void TimerSensorCB(void * data, SoSensor *)
{
UpdateParticle();
if(rainbow && (delay > 25))
{
delay = 0;
col++; // Change The Particle Color
if (col > 11)
col = 0; // If Color Is To High Reset It
}
delay++;
}
创建场景数据
void BuildScene(void)
{
//添加键盘回调事件
SoEventCallback* pEventCallback = new SoEventCallback;
pEventCallback->addEventCallback(SoKeyboardEvent::getClassTypeId(),
KeyboardEventCB,g_pOivSceneRoot);
g_pOivSceneRoot->addChild(pEventCallback);
//OpenGL回调事件
SoCallback *pGlCallback = new SoCallback();
pGlCallback->setCallback(GlRenderCB, NULL);
g_pOivSceneRoot->addChild(pGlCallback);
SoComplexity *pComplexity = new SoComplexity;
pComplexity->textureQuality = 0.6;
g_pOivSceneRoot->addChild(pComplexity);
//加载材质位图
SoTexture2 *Texture = new SoTexture2;
Texture->filename.setValue("../Data/Particle.png");
Texture->model = SoTexture2::MODULATE;
g_pOivSceneRoot->addChild(Texture);
SoMaterialBinding *pMaterialBinding = new SoMaterialBinding;
pMaterialBinding->value = SoMaterialBindingElement::PER_PART;
g_pOivSceneRoot->addChild(pMaterialBinding);
g_pParticleColor = new SoMaterial;
g_pOivSceneRoot->addChild(g_pParticleColor);
g_pParticleCoord = new SoCoordinate3;
g_pOivSceneRoot->addChild(g_pParticleCoord);
g_pParticleTexCoord = new SoTextureCoordinate2;
g_pOivSceneRoot->addChild(g_pParticleTexCoord);
g_pParticleStripSet = new SoTriangleStripSet;
for (int i = 0; i < MAX_PARTICLES; i++)
ParticleNumVertices[i] = 4;
g_pParticleStripSet->numVertices.setValues(0,MAX_PARTICLES,ParticleNumVertices);
g_pOivSceneRoot->addChild(g_pParticleStripSet);
//初始化粒子状态数组
for (int i = 0; i < MAX_PARTICLES; i++) // Initials All The Textures
{
particle[i].active = true; // Make All The Particles Active
particle[i].life = 1.0f; // Give All The Particles Full Life
particle[i].fade = float(rand() % 100) / 1000.0f + 0.003f; particle[i].r = colors[int(i * (12.0 / MAX_PARTICLES))][0]; particle[i].g = colors[int(i * (12.0 / MAX_PARTICLES))][1]; particle[i].b = colors[int(i * (12.0 / MAX_PARTICLES))][2]; particle[i].xi = float((rand() % 50) - 26.0f) * 10.0f; particle[i].yi = float((rand() % 50) - 25.0f) * 10.0f;
particle[i].zi = float((rand() % 50) - 25.0f) * 10.0f; particle[i].xg = 0.0f; // Set Horizontal Pull To Zero
particle[i].yg = -0.8f; // Set Vertical Pull Downward
particle[i].zg = 0.0f; // Set Pull On Z Axis To Zero
particle[i].x = 0;
particle[i].y = 0;
particle[i].z = 0;
}
UpdateParticle();
//定义粒子更新定时器
SoTimerSensor *pTimerSensor = new SoTimerSensor(TimerSensorCB, NULL);
pTimerSensor->setInterval(0.001);
pTimerSensor->schedule();
}
下面的代码是键盘响应函数。
void KeyboardEventCB(void *userData, SoEventCallback *pEventCB)
{
const SoEvent *pEvent = pEventCB->getEvent();
if(SO_KEY_PRESS_EVENT(pEvent,LEFT_ARROW))
{
if(xspeed > -200)
xspeed -= 1.0f;
}
else
if(SO_KEY_PRESS_EVENT(pEvent,RIGHT_ARROW))
{
if(xspeed < 200)
xspeed += 1.0f;
}
else
if(SO_KEY_PRESS_EVENT(pEvent,UP_ARROW))
{
if(yspeed < 200)
yspeed += 1.0f;
}
else
if(SO_KEY_PRESS_EVENT(pEvent,DOWN_ARROW))
{
if(yspeed > -200)
yspeed -= 1.0f;
}
else
if(SO_KEY_PRESS_EVENT(pEvent,PAGE_UP))
{
zoom += 0.1f;
}
else
if(SO_KEY_PRESS_EVENT(pEvent,PAGE_DOWN))
{
zoom -= 0.1f;
}
else
if(SO_KEY_PRESS_EVENT(pEvent,PAD_ADD))
{
if(slowdown > 1.0f)
slowdown -= 0.01f;
}
else
if(SO_KEY_PRESS_EVENT(pEvent,PAD_SUBTRACT))
{
if(slowdown < 4.0f)
slowdown += 0.01f;
}
else
if(SO_KEY_PRESS_EVENT(pEvent,TAB))
{
for (int i = 0; i < MAX_PARTICLES; i++)
{
if (particle[i].active) // If The Particle Is Active
{
particle[i].x = 0.0f; // Center On X Axis
particle[i].y = 0.0f; // Center On Y Axis
particle[i].z = 0.0f; // Center On Z Axis
particle[i].xi = float((rand() % 50) - 26.0f) * 10.0f; particle[i].yi = float((rand() % 50) - 25.0f) * 10.0f; particle[i].zi = float((rand() % 50) - 25.0f) * 10.0f;
}
}
}
else
if(SO_KEY_PRESS_EVENT(pEvent,PAD_8))
{
for (int i = 0; i < MAX_PARTICLES; i++)
{
if (particle[i].active && particle[i].yg < 1.5f)
particle[i].yg += 0.01f;
}
}
else
if(SO_KEY_PRESS_EVENT(pEvent,PAD_2))
{
for (int i = 0; i < MAX_PARTICLES; i++)
{
if (particle[i].active && particle[i].yg > -1.5f)
particle[i].yg -= 0.01f;
}
}
else
if(SO_KEY_PRESS_EVENT(pEvent,PAD_6))
{
for (int i = 0; i < MAX_PARTICLES; i++)
{
if (particle[i].active && particle[i].xg < 1.5f)
particle[i].xg += 0.01f;
}
}
else
if(SO_KEY_PRESS_EVENT(pEvent,PAD_4))
{
for (int i = 0; i < MAX_PARTICLES; i++)
{
if (particle[i].active && particle[i].xg > -1.5f)
particle[i].xg -= 0.01f;
}
}
else
if(SO_KEY_PRESS_EVENT(pEvent,RETURN))
{
rainbow = !rainbow;
}
else
if(SO_KEY_PRESS_EVENT(pEvent,SPACE))
{
rainbow = false;
delay = 0;
col++; // Change The Particle Color
if (col > 11)
col = 0; // If Color Is To High Reset It
}
pEventCB->setHandled();
}
现在编译运行我们程序,屏幕上显示出一个效果非常华丽的喷泉。按下左右方向键,喷泉可以左右转动。按下上下方向键,喷泉可以上下转动。按下PnUp/PnDn键,喷泉将放大或缩小。按下TAB键,喷泉将初始化一次。按下空格键,喷泉将修改颜色。效果和NeHe第十九课是相同的。
本课的完整代码下载。(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协议的条件下自由使用、修改本文的代码。水平的原因,代码可能不是最优化的,我随时期待读者的指正和交流。转载请注明。谢谢。
我的联系方式:
E-mail: < openinventor@gmail.com > < openinventor@126.com >
Blog: < http://blog.youkuaiyun.com/RobinHao >
Site: < http://www.openinventor.cn >