用OpenInventor实现的NeHe OpenGL教程-第二十课
这节课我们将讨论蒙板技术。所谓蒙板技术可能很多人在2D绘图程序中已经使用过了。例如,我们希望在背景图片上绘制一个人物(精灵)。因为人物的图片是矩形的,而人物本身又是不规则图形,所以矩形图片中会有一些空白的部分。如果我们不将这些空白的部分去掉,直接绘制人物图片的话,程序的效果肯定会很差。这时我们就需要使用蒙板技术了,首先要产生一个和人物图片一样的黑白掩码图片,然后先让这幅黑白掩码图片与背景图片进行异或操作,然后再将真正的人物图像与背景图片进行与操作。这样在背景图片上就会显示出一个“干净”的人物了。
在3D程序中使用的蒙板技术和2D类似,主要是用在纹理映射上。其原理是相同的。
下面的代码首先定义一些全局变量。
SoTextureCoordinate2* g_pLogTexCoord = NULL;//背景纹理坐标
SoTextureCoordinate2* g_pScene1MaskTexCoord = NULL;//场景1掩码纹理坐标
SoTextureCoordinate2* g_pScene1TexCoord = NULL;//场景1纹理坐标
SoSeparator* g_pTopSceneSep = NULL;//场景节点
int g_iWhichScene = 0;//指定显示那个场景。和NeHe教程中的变量含义相同
bool g_bMasking = true;//指定是否显示掩码。和NeHe教程中的变量含义相同
下面的函数用来创建背景场景。背景场景只是一个带有纹理的正方形平面。
SoSeparator* BuildLogSep(void)
{
SoSeparator *pLogSep = new SoSeparator;
SoComplexity *pTextureComplexity = new SoComplexity;
pTextureComplexity->textureQuality = 1.0;
pLogSep->addChild(pTextureComplexity);
SoTexture2 *pLogoTexture = new SoTexture2;
pLogoTexture->filename.setValue("../Data/logo.png");
pLogoTexture->model = SoTexture2::DECAL;
pLogSep->addChild(pLogoTexture);
g_pLogTexCoord = new SoTextureCoordinate2;
g_pLogTexCoord->point.set1Value(0,0,0);
g_pLogTexCoord->point.set1Value(1,3,0);
g_pLogTexCoord->point.set1Value(2,3,3);
g_pLogTexCoord->point.set1Value(3,0,3);
pLogSep->addChild(g_pLogTexCoord);
SoCoordinate3 *pLogCoord = new SoCoordinate3;
pLogCoord->point.set1Value(0, -1.1f , -1.1f , 0.0f );
pLogCoord->point.set1Value(1, 1.1f , -1.1f , 0.0f );
pLogCoord->point.set1Value(2, 1.1f , 1.1f , 0.0f );
pLogCoord->point.set1Value(3, -1.1f , 1.1f , 0.0f );
pLogSep->addChild(pLogCoord);
pLogSep->addChild(new SoFaceSet);
return pLogSep;
}
下面的函数用来创建场景1。场景1也是一个带有纹理的正方形平面。不过它可以根据bMasking变量来决定是否创建掩码正方形。
SoSeparator* BuildScene1Sep(bool bMasking)
{
SoSeparator *pScene1Sep = new SoSeparator;
if (bMasking)
{
SoTexture2 *pScene1MaskTexture = new SoTexture2;
pScene1MaskTexture->filename.setValue("../Data/mask1.png");
pScene1MaskTexture->model = SoTexture2::DECAL;
pScene1Sep->addChild(pScene1MaskTexture);
g_pScene1MaskTexCoord = new SoTextureCoordinate2;
g_pScene1MaskTexCoord->point.set1Value(0,0,0);
g_pScene1MaskTexCoord->point.set1Value(1,4,0);
g_pScene1MaskTexCoord->point.set1Value(2,4,4);
g_pScene1MaskTexCoord->point.set1Value(3,0,4);
pScene1Sep->addChild(g_pScene1MaskTexCoord);
SoCoordinate3 *pScene1MaskCoord = new SoCoordinate3;
pScene1MaskCoord->point.set1Value(0, -1.1f , -1.1f , 0.0f );
pScene1MaskCoord->point.set1Value(1, 1.1f , -1.1f , 0.0f );
pScene1MaskCoord->point.set1Value(2, 1.1f , 1.1f , 0.0f );
pScene1MaskCoord->point.set1Value(3, -1.1f , 1.1f , 0.0f );
pScene1Sep->addChild(pScene1MaskCoord);
pScene1Sep->addChild(new SoFaceSet);
}
SoCallback *pOneGlCallback = new SoCallback();
pOneGlCallback->setCallback(GlRenderCB, (void *)2);
pScene1Sep->addChild(pOneGlCallback);
SoTexture2 *pScene1Texture = new SoTexture2;
pScene1Texture->filename.setValue("../Data/image1.png");
pScene1Sep->addChild(pScene1Texture);
g_pScene1TexCoord = new SoTextureCoordinate2;
g_pScene1TexCoord->point.set1Value(0,0,0);
g_pScene1TexCoord->point.set1Value(1,4,0);
g_pScene1TexCoord->point.set1Value(2,4,4);
g_pScene1TexCoord->point.set1Value(3,0,4);
pScene1Sep->addChild(g_pScene1TexCoord);
SoCoordinate3 *pScene1Coord = new SoCoordinate3;
pScene1Coord->point.set1Value(0, -1.1f , -1.1f , 0.0f );
pScene1Coord->point.set1Value(1, 1.1f , -1.1f , 0.0f );
pScene1Coord->point.set1Value(2, 1.1f , 1.1f , 0.0f );
pScene1Coord->point.set1Value(3, -1.1f , 1.1f , 0.0f );
pScene1Sep->addChild(pScene1Coord);
pScene1Sep->addChild(new SoFaceSet);
return pScene1Sep;
}
下面的函数用来创建场景2。场景2的内容和场景1类似
SoSeparator* BuildScene2Sep(bool bMasking)
{
SoSeparator *pScene2Sep = new SoSeparator;
SoTranslation *pInitTrans = new SoTranslation;
pInitTrans->translation.setValue(0,0,-1);
pScene2Sep->addChild(pInitTrans);
SoRotor *pRotor = new SoRotor;
pRotor->speed = 0.1;
pScene2Sep->addChild(pRotor);
if (bMasking)
{
SoTexture2 *pScene2MaskTexture = new SoTexture2;
pScene2MaskTexture->filename.setValue("../Data/mask2.png");
pScene2MaskTexture->model = SoTexture2::DECAL;
pScene2Sep->addChild(pScene2MaskTexture);
SoTextureCoordinate2 *pScene2MaskTexCoord = new SoTextureCoordinate2;
pScene2MaskTexCoord->point.set1Value(0,0,0);
pScene2MaskTexCoord->point.set1Value(1,1,0);
pScene2MaskTexCoord->point.set1Value(2,1,1);
pScene2MaskTexCoord->point.set1Value(3,0,1);
pScene2Sep->addChild(pScene2MaskTexCoord);
SoCoordinate3 *pScene2MaskCoord = new SoCoordinate3;
pScene2MaskCoord->point.set1Value(0, -1.1f , -1.1f , 0.0f );
pScene2MaskCoord->point.set1Value(1, 1.1f , -1.1f , 0.0f );
pScene2MaskCoord->point.set1Value(2, 1.1f , 1.1f , 0.0f );
pScene2MaskCoord->point.set1Value(3, -1.1f , 1.1f , 0.0f );
pScene2Sep->addChild(pScene2MaskCoord);
pScene2Sep->addChild(new SoFaceSet);
}
SoCallback *pOneGlCallback = new SoCallback();
pOneGlCallback->setCallback(GlRenderCB, (void *)2);
pScene2Sep->addChild(pOneGlCallback);
SoTexture2 *pScene2Texture = new SoTexture2;
pScene2Texture->filename.setValue("../Data/image2.png");
pScene2Sep->addChild(pScene2Texture);
SoTextureCoordinate2 *pScene2TexCoord = new SoTextureCoordinate2;
pScene2TexCoord->point.set1Value(0,0,0);
pScene2TexCoord->point.set1Value(1,1,0);
pScene2TexCoord->point.set1Value(2,1,1);
pScene2TexCoord->point.set1Value(3,0,1);
pScene2Sep->addChild(pScene2TexCoord);
SoCoordinate3 *pScene2Coord = new SoCoordinate3;
pScene2Coord->point.set1Value(0, -1.1f , -1.1f , 0.0f );
pScene2Coord->point.set1Value(1, 1.1f , -1.1f , 0.0f );
pScene2Coord->point.set1Value(2, 1.1f , 1.1f , 0.0f );
pScene2Coord->point.set1Value(3, -1.1f , 1.1f , 0.0f );
pScene2Sep->addChild(pScene2Coord);
pScene2Sep->addChild(new SoFaceSet);
return pScene2Sep;
}
开始构造完整的场景
void BuildScene(void)
{
//增加键盘响应节点
SoEventCallback* pEventCallback = new SoEventCallback;
pEventCallback->addEventCallback(SoKeyboardEvent::getClassTypeId(),KeyboardEventCB,g_pOivSceneRoot);
g_pOivSceneRoot->addChild(pEventCallback);
//整个场景向Z轴负方向移动2个单位
SoTranslation *pInitTrans = new SoTranslation;
pInitTrans->translation.setValue(0,0,-2);
g_pOivSceneRoot->addChild(pInitTrans);
//增加上背景场景
g_pOivSceneRoot->addChild(BuildLogSep());
//定义GL渲染方式
SoCallback *pEnableGlCallback = new SoCallback();
pEnableGlCallback->setCallback(GlRenderCB, 0);
g_pOivSceneRoot->addChild(pEnableGlCallback);
//创建场景1
g_pTopSceneSep = new SoSeparator;
g_pOivSceneRoot->addChild(g_pTopSceneSep);
g_pTopSceneSep->addChild(BuildScene1Sep(g_bMasking));
SoCallback *pDisableGlCallback = new SoCallback();
pDisableGlCallback->setCallback(GlRenderCB, (void *)1);
g_pOivSceneRoot->addChild(pDisableGlCallback);
//增加定时器节点,用来移动场景中纹理坐标
SoTimerSensor * texttimer = new SoTimerSensor(TimerSensorCB, NULL);
texttimer->setInterval(0.001);
texttimer->schedule();
}
GL渲染回调函数,渲染方式和NeHe教程中的相同。
void GlRenderCB(void *data, SoAction *action)
{
if (action->isOfType(SoGLRenderAction::getClassTypeId()))
{
switch((int)data)
{
case 0:
glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
if (g_bMasking)
glBlendFunc(GL_DST_COLOR,GL_ZERO); // Blend Screen Color With Zero (Black)
break;
case 1:
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
break;
case 2:
glBlendFunc(GL_ONE, GL_ONE);
break;
}
}
}
时间回调函数,用来修改纹理坐标,产生动画效果
void TimerSensorCB(void * data, SoSensor *)
{
static float roll = 0;
roll += 0.002f ; // Increase Our Texture Roll Variable
if (roll > 1.0f ) // Is Roll Greater Than One
roll -= 1.0f ; // Subtract 1 From Roll
g_pLogTexCoord->point.set1Value(0,0,-roll + 0);
g_pLogTexCoord->point.set1Value(1,3,-roll + 0);
g_pLogTexCoord->point.set1Value(2,3,-roll + 3);
g_pLogTexCoord->point.set1Value(3,0,-roll + 3);
if(g_iWhichScene == 0)
{
if (g_bMasking)
{
g_pScene1MaskTexCoord->point.set1Value(0,roll + 0,0);
g_pScene1MaskTexCoord->point.set1Value(1,roll + 4,0);
g_pScene1MaskTexCoord->point.set1Value(2,roll + 4,4);
g_pScene1MaskTexCoord->point.set1Value(3,roll + 0,4);
}
g_pScene1TexCoord->point.set1Value(0,roll + 0,0);
g_pScene1TexCoord->point.set1Value(1,roll + 4,0);
g_pScene1TexCoord->point.set1Value(2,roll + 4,4);
g_pScene1TexCoord->point.set1Value(3,roll + 0,4);
}
}
键盘响应函数,用来切换场景
void KeyboardEventCB(void *userData, SoEventCallback *pEventCB)
{
const SoEvent *pEvent = pEventCB->getEvent();
if(SO_KEY_PRESS_EVENT(pEvent,SPACE))
{
g_iWhichScene = (++g_iWhichScene) % 2;
g_pTopSceneSep->removeAllChildren();
if (g_iWhichScene == 0)
g_pTopSceneSep->addChild(BuildScene1Sep(g_bMasking));
else
g_pTopSceneSep->addChild(BuildScene2Sep(g_bMasking));
}
else
if(SO_KEY_PRESS_EVENT(pEvent,M))
{
g_bMasking = !g_bMasking;
g_pTopSceneSep->removeAllChildren();
if (g_iWhichScene == 0)
g_pTopSceneSep->addChild(BuildScene1Sep(g_bMasking));
else
g_pTopSceneSep->addChild(BuildScene2Sep(g_bMasking));
}
pEventCB->setHandled();
}
现在编译运行我们程序,屏幕上显示出向上移动的背景图片和一个向左移动的镂空的前景图片。按下空格键,将变换场景。按下M键会取消掩码效果。程序运行的效果和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 >