作者归档:tsai

怎样使一个IP手游的第一次CBT拖延3个月

最近,参与的IP手游项目终于第一次CBT结束了。

IP手游真是件累人的事儿,各种监修各种限制,光游戏UI资源就换了至少4个版本,各种比较创新的玩法也被一一砍掉。本来的CBT时间点是在3月份的,然而,现在是7月份了。不得不喷下这大几个月在拥有众多资源的公司中开发IP手游碰到的问题。

  • 前期规划不足

规划的问题,其实是在各种项目中多多少少都会碰到的问题。既然负责客户端这边的开发,就只说说客户端统一性规划。涉及人员当然是策划、美术、程序 缺一不可。

项目开发过程中,我们的策划的每一个案子中控件的设置方法、摆放位置(涉及美术),乃至配置表字段的设计都是乱来的,怎么舒服怎么做,导致美术出的效果也不能统一,然后美术的设计风格也是一个界面一风格。如此,直到游戏基本成型后还需要重新大幅调整资源,统一设计风格,然后程序加班加点修改,这上面浪费了大量的时间精力。如果在项目初期就注意这些统一性的把控,游戏开发应该是更顺畅的体验吧?

  • 开发迭代不规范

再者,项目开发过程中,美术、程序的迭代过程不是不断优化的过程,而是不断替换之前成果,更关键是这种替换不是推到重来,而是回到之前的之前版本模式。

糟糕的迭代导致项目资源的严重浪费,我看到程序、美术在不断加班加点做着重复的工作,感觉好痛心。我在考虑一个问题,制作人在这种情况下是不是应该停歇一下,思考下项目为什么会进入这样恶性循环中,而不是任由此类态势发展下去,最终导致项目不断延期。

当然,这种无奈的情况不能排除IP游戏中特有的情况,如若真是如此,是不是负责监修方面的人员应该做的更到位一点?

  • 产品阶段模糊

在我们游戏CBT版本功能还没有定下的时候,就开始接入QA了。然后QA各种测试后,提交了各种bug,程序再修复bug,如此反复一个多月,直到一个多月后,才发现QA测试的很多功能是要砍掉的或是程序、美术要重新修改的功能。

项目开发中也频现类似的情况:策划分配策划案到美术、程序,之后相关创作人员进行功能制作。在程序获得美术资源进行功能开发的时候,美术的第二版、第三版资源就接踵而至,导致程序浪费大量时间在前段ui修改上,而不是代码性能及功能优化上。

诸如以上产品把控的乱入,直接造成项目资源的严重浪费。所以,想拖延项目进程的,照着上面做就行了。

2014->2015

最近跟朋友聊天,总或多或少听到提及时间过得好快啊。。之类的词语,巧的是,本人也有同样的感觉。

即将过去的一年,对于我的关键词简约了好多:生娃、手游。

生娃

我的宝贝女儿终于在这一年健健康康地来了,开始了她的精彩人生,我也算正式加入奶爸大军了。现在每天回家,还是只能跟他大眼瞪小眼,对视地傻笑,不过这样就够了。生之前总有计划着,生完后到现在,执行一片空白,突然发现这真是一种病,得改。

手游

本来想把做手游当成职业爱好,不想竟然真的做了一年多了,而且还真的做出了像样的作品了,而且成绩还算可以(对于处女作来说)。

虫虫消消乐没推广的情况下,在苹果AppStore上卖出了近3万拷贝,算是达到了自己的预期。但是在随后的版本更新中,自己引入了一个悔到肠青的bug,导致用户量和活跃用户急剧下降,希望吃了这一堑得长点智,修改bug,还是得有个完整的测试过程。

在google play上,虫虫消消乐卖了近5000拷贝,成绩不好的主要原因,我就归结为Android版本出得较晚,还有google play在大陆众所周知的处境。因为游戏中有内购功能,国内安卓平台限制个人应用集成内购功能,因此,Android版本没在国内的安卓平台上线,这应该也算Android版本不佳的原因吧。

关于吹过的牛逼

写了几年总结,发现年年都会吹一些牛逼,然后下一年终总有对不起的牛逼。

吹过的牛逼1——业余时间做2-3个手游。我其实业余只做了两个简单手游,一个虫虫消消乐,另一个也是消除类游戏,当然不是虫虫消消乐的玩法。但是,没上线,由于种种原因。。好吧,这是一种病。

吹过的牛逼2——再多看四五本书。其实是很简单的一件事,但是竟然还是没达成。现在想想,我总以太专注写代码来搪塞“为什么没时间看书”这样的理由,这也是加剧拖延症的主要原因之一,得治。

各种年度评选

今年年度人物毫无疑问是我的乖乖女儿——水灵灵的眼睛眨呀眨。

年度产品就将就发给虫虫消消乐吧。

2015

  • 看10本书;
  • 写12篇文章;
  • 努力赚钱,养家糊口;
  • 对得起吹过的牛逼;
  • 与拖延症顽抗到底。

豪华经典三消游戏:Happy Bugs — 虫虫消消乐

Happy Bugs

Download_on_the_App_Store_Badge

en_generic_rgb_wo_45

androidDown45

经典的操作,独特的玩法,​超乎寻常的体验。

本年度最吸引眼球的昆虫消除游戏!

还在玩天天爱消除,天天联萌,开心消消乐,糖果粉碎,打飞机,猜图,猜歌,猜成语,植物大战僵尸?来点新鲜的吧!

游戏简介:
小伙伴们在各自的旅程中,碰到了多种多样的可爱虫,大家用智慧捕获各式各样的虫虫,并在过程中收集了各种水果蔬菜。
游戏中,你只要把三个或三个以上相同的虫虫连在同一直线上就可以消除,你可能要同时清除虫虫粘液,经受困住虫虫的各种障碍物的挑战。
为了完成任务,你需要克服重重障碍,你也可以使用各种道具来降低难度。
别忘了去收集金币哦,有了金币,你可以购买很多有意思的道具。

IMG_0776IMG_0777IMG_0788

游戏特色:

不一样的旅程,数百个关卡,从邻居家的小院子到郊区公园、湿地公园,再到杰克的蘑菇园!
魔法小虫:游戏中,一次消除尽量多的的小虫,将产生各种各样的魔法小虫,让你在游戏中更加畅快。
小虫粘液:游戏中清理各种小虫粘液。
小虫铁网:游戏中,打掉困住小虫的铁网。
绿色阻碍:通过消除打掉小虫旁的绿色锁链。
僵尸阻碍:通过消除打掉小虫旁边自动产生的僵尸障碍,把它们从你的视线中清除!
水果、蔬菜收集:在游戏中收集目标水果和蔬菜。
7种道具,多种可爱小虫等你来获取。
更有魔法能量助你一臂之力!
上百种游戏地图,让对对碰玩出更多花样,更多乐趣!
丰富形象的帮助以及新手引导,让你一分钟轻松上手,从菜鸟变身无敌专家!
还有更多有趣的玩法在等着您!

更多惊喜:
支持高清视网膜屏幕。
全设备支持,在你的ipad,iphone,ipod touch相同体验游戏乐趣。
更多玩法即将加入,更多惊喜等你来发掘。

这就是最新体验、最具挑战的消除类游戏,超越经典之神作 – 虫虫消消乐。
还在犹豫什么? 赶紧进入挑战吧。

如果小伙伴们对 虫虫爱消除 有什么问题,请留下评论哦,保证能愉快地玩耍哦。

Switch and match colorful bugs through hundreds of level in this puzzle adventure!! Join and enjoying Happy Bugs completely FREE!

*WHY YOU’LL LOVE HAPPY BUGS*
- Easy and fun to play, but hard to be master!
- Amazing music and sound effects!
- Colorful graphics!
- Hundreds of funny levels, every of them is unique !
- FREE updates with new puzzles.

Happy bugs is completely free to play but some in-game items will require payment. Just disable in app purchases on your device if you don’t want to use this feature.

Play Happy bugs today and join millions enjoying the latest touch craze!

Plz leave comments if you have any questions.

cocos2d-x 截图保存到本地

游戏推广的一个很重要组成就是玩家分享,所以游戏截图就起到很大作用了。截图功能通过CCRenderTexture实现。

1.CCRenderTexture

CCRenderTexture是一个通用渲染对象,可以通过构建一个CCRenderTexture对象,把要渲染的东西填充进去,在渲染开始前调用call函数,调用cocos的场景的visit函数对其进行渲染,渲染结束后调用end函数。

CCRenderTexture继承于CCNode,所以可以简单地把渲染纹理添加到你的场景中,就像处理其它cocos中的节点一样,当然它还提供了保存功能,可以把渲染纹理保存为PNG或JPG格式。

在实际应用中中,只是获取渲染的纹理还不够,还要考虑不同尺寸设备的截图范围,当然,这个是针对采用kResolutionNoBorder屏幕适配方案来说,下面的粒子就是针对kResolutionNoBorder。

2.API

//创建和初始化函数
   static CCRenderTexture * create(int w ,int h, CCTexture2DPixelFormat eFormat, GLuint uDepthStencilFormat);
   static CCRenderTexture * create(int w, int h, CCTexture2DPixelFormat eFormat);
   static CCRenderTexture * create(int w, int h);
   bool initWithWidthAndHeight(int w, int h, CCTexture2DPixelFormat eFormat);
   bool initWithWidthAndHeight(int w, int h, CCTexture2DPixelFormat eFormat, GLuint uDepthStencilFormat);

   //开始获取
   void begin();

   //开始渲染时清除之前渲染的内容
   void beginWithClear(float r, float g, float b, float a);
   void beginWithClear(float r, float g, float b, float a, float depthValue);
   void beginWithClear(float r, float g, float b, float a, float depthValue, int stencilValue);

   //结束获取
   void end();

   //清除纹理
   void clear(float r, float g, float b, float a);
   void clearDepth(float depthValue);
   void clearStencil(int stencilValue);

   //保存纹理为图片文件,可以选择JPG/PNG格式,默认是JPEG格式,成功返回真
   bool saveToFile(const char *szFilePath);
   bool saveToFile(const char *name, tCCImageFormat format);

3.示例

修改HelloWorld中结束菜单的回调函数如下:


void CTestLayer::menuCloseCallback(CCObject* pSender)
{
    SaveScreenShot();
}

//截图功能
void CTestLayer::SaveScreenShot()
{
 //获取屏幕尺寸
//CCDirector::sharedDirector()->getVisibleSize();
CCSize size =  CCDirector::sharedDirector()->getWinSize();
CCPoint origin = VisibleRect::getVisibleRect().origin;
CCSize visableSize = VisibleRect::getVisibleRect().size;

//使用屏幕尺寸初始化一个空的渲染纹理对象
CCRenderTexture* texture = CCRenderTexture::create((int)size.width, (int)size.height);

//设置位置
texture->setPosition(ccp(size.width/2, size.height/2));
CCScene* curScene = CCDirector::sharedDirector()->getRunningScene();

//开始获取
texture->begin();

//遍历场景节点对象,填充纹理到texure中
curScene->visit();

//结束获取
texture->end();

//保存为PNG图
if (origin.x == 0 && origin.y == 0) {
texture->saveToFile("screenshot.png", kCCImageFormatPNG);
return;
}

// 适配不同尺寸设备,只截取屏幕大小的尺寸,没采用kResolutionNoBorder就不用下面的处理

CCRect finalRect = CCRectMake(origin.x, origin.y, visableSize.width, visableSize.height);
CCSprite *sprite = CCSprite::createWithTexture(texture->getSprite()->getTexture(), finalRect);
sprite->setAnchorPoint(CCPoint(0, 0));
sprite->setFlipY(true);
CCRenderTexture *finalRtx = CCRenderTexture::create(visableSize.width, visableSize.height);
finalRtx->begin();
sprite->visit();
finalRtx->end();
finalRtx->saveToFile("screenshot.png", kCCImageFormatPNG);
}

写在201314

照旧,在2013的最后一天是该总结点东西。这个时候,整理那些思绪中混乱的时间碎片,都会感伤时间的飞快。先亮一下现在头壳中的那些标签云吧:游戏、微信、苹果、小米、3d打印、移动开发。这些是我今年在reader中关注比较多的词条。

可能因为有点过分专注于写代码了,这是沉溺在reader、微博、知乎比较少的一年了。简单地结个论,这是属于微信的一年,也是属于小米的一年,同样也是各种移动应用的爆发年,这行的国产企业的雄起,让我不用预测都知道2014它们会更加火热,加上易信、来往,各种支付以及各种手机厂商、各种云的乱入,2014将会上演更加精彩的业内诸企业争霸剧情,都忍不住想看了。

戏是大佬们演的好看,但还是得回到现实演好自己的屌丝泡沫剧。还是评评本屌丝泡沫剧的各种年度最佳抢镜:

年度抢镜关键词

  • 游戏:我有史以来专注在游戏行业上最多的一年,也更多地主动接触横跨端游、页游、手游各种游戏(我之前可不是个游戏迷)。工作上,负责away3d粒子编辑器编写,参与游戏编辑器的功能开发,这些都让我收获蛮多,业余上,基于cocos2d-x写了个三消游戏,也算学习了手游开发的流程,弥补了自己移动端的不足。

年度抢镜电子产品

今年是最低碳的一年,只添置了Amazon的kindle paperwhite,虽然仍有很多很酷的产品上线,但是貌似没促动我消费神经的产品?好吧,我不会告诉你其实是我没钱。。

  • mbp:这是前年的东东了,但是其实人家还没过时呢。装上osx10.9,各种崭新如初,写代码什么的各种方便,伦家unix系的就是霸道就是逼格高。额低调下,其实挺卡有时候,有钱了咱也升级。
  • ipad2:这是去年的,但是人家也没过时呢。升级ios7,依然各种顺畅崭新如初,各种场合看片游戏利器,加之百度网盘乱入,妈妈再也不用担心我看片不方便了。
  • kindle Paperwhite:其实这是老婆建议买的,但是万万没想到,因为翻页的拖延及闪烁,用惯了ipad的年轻人用起来不习惯,买后几乎是我在用,真是不幸中的万幸。借助它,我算是多看了点书(但是貌似还没把本钱看回来),而且看网页文章各种爽,还是得赞下amazon的send to kindle插件。

年度抢镜互联网产品

本来想分开pc和移动的,但是界限太模糊,不好操作。随着移动互联网的大势入侵,pc互联网很多都加大了移动互联网的打通力度,静候2014各种大战。

  • 微信:这个应该没有争议,因为它和新浪微博在国内大有统治之势头。在得入口者得天下这句话的号召下,如今业内可谓群雄并起,今年的第四季度易信、来往的突袭可见一斑。
  • 支付宝:新推出的余额宝让大佬们看到了网上支付可以这样玩期货,余额宝的成功将挑起更多的网上期货大战,诸如度娘的百度理财,加之微信微支付的乱入。唉,一想到这些战争就开心。
  • 百度网盘:打通web、pc、移动端的类dropbox,提供了2T空间让社会各界宅男乐开了花,有了2T的种子,他们的妈再也不用担心他们了吗?
  • 小米:暂且将小米归到互联网产品吧,因为人家不是说了互联网思维做产品嘛。不说什么,就冲3年弄出个100亿美金的公司,雷牛了。

年度抢镜业余作品

  • 三消游戏:基于cocos2dx做的一个手游,程序、美工、音效等等都自己包了,好累啊。
  • 微洛克:腾讯微博粉丝管理工具,帮微博运营的同学写的一个小工具。
  • 微信自动回复公共账号:微信号:musictee,叫我爱查歌词,提供歌词查询功能。

年度抢镜旅游

说到旅游,得说今年是悲催的一年,也就省内溜溜。

  • 福鼎太姥山:跟老婆和她得小伙伴们去的,因做为拎包员得以跟随。其实挺漂亮的那景色,要说也不比武夷山差多少,就是开发没跟上。

年度抢镜户外活动

  • 全程马拉松:参加了厦门国际马拉松,具体跑了多少就不说了,反正咱是在7个小时内,拿了牌的呢。好吧,还是说说吧,6个多小时,但是我是有理由的,中途吃了顿肯德基来着,不然,不然,走慢点也是6个多小时。说实话吧,都走那么慢了,脚还是酸了一周多的。

年度抢镜人物

  • 我老婆,还有我们的宝宝,要好好健康成长。
  • 老爸老妈,他们在变老,依旧奋战一线,做为儿子很是惭愧。

展望2014

  • 业余时间做2-3个手游;
  • 再多看四五本书;
  • 用好时间规划软件;
  • 努力赚钱,养家糊口。

away3d传送门特效

away3d 4.0加入特效系统后,做比较炫的特效已经不是什么难事,away3d官方已经基于内置的particle system做了几个挺炫的粒子特效,需要的可以查看github中实例源码。这个传送门特效且作为away3d 粒子系统练手的的另一个实例。

简单说下,这个特效由三个独立的部分组成,转动的符文圆盘、向上发射的半透明光及不断从符文盘向上浮动的颗粒。效果图如下:

Snip201303_3

废话不说,直接上代码解释。

首先,必须准备三个部分的纹理和材质了。分别是符文圆盘、光辉、浮动粒子的纹理,在本教程最后提供的源码里有相应的纹理。

[Embed(source="../assets/pan.png")]
public  var panTexture:Class;

[Embed(source="../assets/light.png")]
public  var lightTexture:Class;

[Embed(source="../assets/blueball.png")]
public  var blueballTexture:Class;

材质拆分成五个部分,分别对应五种不同的粒子mesh。简单说为什么有五种,因为:1圆盘+3光辉+1浮动粒子串。粒子对应的五种material、mesh、animator等的声明如下,它们之间的关系我们将在后面代码解释。

//materials
private var matPan:TextureMaterial;
private var matLight1:TextureMaterial;
private var matLight2:TextureMaterial;
private var matLight3:TextureMaterial;
private var matBall:TextureMaterial;

//particle objects
private var particleAnimationSet1:ParticleAnimationSet;
private var particleAnimationSet2:ParticleAnimationSet;
private var particleAnimationSet3:ParticleAnimationSet;
private var particleAnimationSet4:ParticleAnimationSet;
private var particleAnimationSet5:ParticleAnimationSet;
private var particleGeometry1:ParticleGeometry;
private var particleGeometry2:ParticleGeometry;
private var particleGeometry3:ParticleGeometry;
private var particleGeometry4:ParticleGeometry;
private var particleGeometry5:ParticleGeometry;

// scene objects
private var particleMesh1:Mesh;
private var particleMesh2:Mesh;
private var particleMesh3:Mesh;
private var particleMesh4:Mesh;
private var particleMesh5:Mesh;
private var animator1:ParticleAnimator;
private var animator2:ParticleAnimator;
private var animator3:ParticleAnimator;
private var animator4:ParticleAnimator;
private var animator5:ParticleAnimator;

接着就是初始化引擎了,为了便于理解,采用跟官方一样的方法,顺便贴出代码。

//engine variables
private var scene:Scene3D;
private var camera:Camera3D;
private var view:View3D;
private var cameraController:HoverController;

//signature variables
private var Signature:Sprite;
private var SignatureBitmap:Bitmap;

//navigation variables
private var _move:Boolean = false;
private var _lastPanAngle:Number;
private var _lastTiltAngle:Number;
private var _lastMouseX:Number;
private var _lastMouseY:Number;

/**
 * Initialise the engine
 */
private function initEngine():void
{
	stage.scaleMode = StageScaleMode.NO_SCALE;
	stage.align = StageAlign.TOP_LEFT;

	scene = new Scene3D();
	camera = new Camera3D();

	view = new View3D();
	view.scene = scene;
	view.camera = camera;

	//setup controller to be used on the camera
	cameraController = new HoverController(camera, null, 255, 50, 300, 5);
	view.addSourceURL("srcview/index.html");
	addChild(view);

	//add signature
	Signature = Sprite(new SignatureSwf());
	SignatureBitmap = new Bitmap(new BitmapData(Signature.width, Signature.height, true, 0));
	stage.quality = StageQuality.HIGH;
	SignatureBitmap.bitmapData.draw(Signature);
	stage.quality = StageQuality.LOW;
	addChild(SignatureBitmap);

	addChild(new AwayStats(view));
}

引擎准备完后就是进入主题了,开始制作各种粒子。away3d中的生成粒子有几个步骤:

1、定义粒子需要的几何体集合,比如该教程中传送门的转动圆盘需要1个plane, 浮动的粒子需要50个plane来表现;

2、根据几何体几何,生成粒子需要的ParticleGeometry;

3、根据ParticleGeometry、Material,生成粒子mesh;

4、定义particleAnimationSet。粒子中的动作节点分为局部静态动作、局部动态动作还有全局动作,一个particleAnimationSet可以添加以上多种类型的不同动作节点;进一步了解动作细节,点击查看官方文档

5、根据particleAnimationSet定义ParticleAnimator,并把ParticleAnimator装到粒子mesh上。

6、设置粒子的start time,duration time, delay time及局部动作属性。

文字太抽象,下面是直观的代码:


		/**
		 * 初始化粒子材质
		 */
		private function initMaterials():void
		{
			matPan = new TextureMaterial(Cast.bitmapTexture(panTexture));
			matBall = new TextureMaterial(Cast.bitmapTexture(blueballTexture));
			matLight1 = new TextureMaterial(Cast.bitmapTexture(lightTexture));
			matLight2 = new TextureMaterial(Cast.bitmapTexture(lightTexture));
			matLight3 = new TextureMaterial(Cast.bitmapTexture(lightTexture));

			matPan.bothSides = true;
			matPan.smooth = true;
			matPan.repeat = true;
			matPan.alphaBlending = true;
			matPan.specularColor = 0xFF1161D9;
			matPan.ambientColor = 0xFF1161D9;

			matLight1.bothSides = true;
			matLight1.smooth = true;
			matLight1.alphaBlending = true;

			matLight2.bothSides = true;
			matLight2.smooth = true;
			matLight2.alphaBlending = true;

			matLight3.bothSides = true;
			matLight3.smooth = true;
			matLight3.alphaBlending = true;

			matBall.bothSides = true;
			matBall.repeat = true;
			matBall.smooth = true;
			matBall.alphaBlending = true;
		}

		/**
		 * 初始化粒子
		 */
		private function initParticles():void
		{
			// 设置所有粒子的particleGeometry
			var ball:Geometry = new PlaneGeometry(3,3,1,1,false);
			var light1:Geometry = new CylinderGeometry(50,30,200,16,1,false,false,true);
			var light2:Geometry = new CylinderGeometry(55,30,180,16,1,false,false,true);
			var light3:Geometry = new CylinderGeometry(60,30,160,16,1,false,false,true);
			var pan:Geometry = new PlaneGeometry(65,65,1,1);

			var ballGeoSet:Vector.<Geometry> = new Vector.<Geometry>();
			var light1GeoSet:Vector.<Geometry> = new Vector.<Geometry>();
			var light2GeoSet:Vector.<Geometry> = new Vector.<Geometry>();
			var light3GeoSet:Vector.<Geometry> = new Vector.<Geometry>();
			var panGeoSet:Vector.<Geometry> = new Vector.<Geometry>();

			// particle ball
			for (var i:int = 0; i < 40; i++) {
				ballGeoSet.push(ball);
			}
			particleGeometry1 = ParticleGeometryHelper.generateGeometry(ballGeoSet);

			particleAnimationSet1 = new ParticleAnimationSet(true, true, false);
			particleAnimationSet1.addAnimation(new ParticleBillboardNode());
			particleAnimationSet1.addAnimation(new ParticleVelocityNode(ParticlePropertiesMode.GLOBAL, new Vector3D(0,30,0)));
			particleAnimationSet1.addAnimation(new ParticleColorNode(ParticlePropertiesMode.GLOBAL,true,true,false,false,new ColorTransform(), new ColorTransform(1,1,1,0)));
			particleAnimationSet1.addAnimation(new ParticlePositionNode(ParticlePropertiesMode.LOCAL_STATIC));
			particleAnimationSet1.addAnimation(new ParticleOscillatorNode(ParticlePropertiesMode.LOCAL_STATIC));
			particleAnimationSet1.addAnimation(new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC));
			particleAnimationSet1.initParticleFunc = initBallParticleProperties;

			// light 1
			light1GeoSet.push(light1);
			particleGeometry2 = ParticleGeometryHelper.generateGeometry(light1GeoSet);
			particleAnimationSet2 = new ParticleAnimationSet(true, true, false);
			particleAnimationSet2.addAnimation(new ParticlePositionNode(ParticlePropertiesMode.GLOBAL, new Vector3D(0,100,0)));
			particleAnimationSet2.addAnimation(new ParticleRotationalVelocityNode(ParticlePropertiesMode.GLOBAL, new Vector3D(0,1,0,25)));
			particleAnimationSet2.addAnimation(new ParticleColorNode(ParticlePropertiesMode.GLOBAL,true,true,false, false,new ColorTransform(1,1,1,0.2), new ColorTransform(1,1,1,0.2)));
			particleAnimationSet2.initParticleFunc = initLightParticleProperties;

			// light 2
			light2GeoSet.push(light2);
			particleGeometry3 = ParticleGeometryHelper.generateGeometry(light2GeoSet);
			particleAnimationSet3 = new ParticleAnimationSet(true, true, false);
			particleAnimationSet3.addAnimation(new ParticlePositionNode(ParticlePropertiesMode.GLOBAL, new Vector3D(0,90,0)));
			particleAnimationSet3.addAnimation(new ParticleRotationalVelocityNode(ParticlePropertiesMode.GLOBAL, new Vector3D(0,1,0,25)));
			particleAnimationSet3.addAnimation(new ParticleColorNode(ParticlePropertiesMode.GLOBAL,true,true,false, false,new ColorTransform(1,1,1,0.2), new ColorTransform(1,1,1,0.2)));
			particleAnimationSet3.initParticleFunc = initLightParticleProperties;

			// light 3
			light3GeoSet.push(light3);
			particleGeometry4 = ParticleGeometryHelper.generateGeometry(light3GeoSet);
			particleAnimationSet4 = new ParticleAnimationSet(true, true, false);
			particleAnimationSet4.addAnimation(new ParticlePositionNode(ParticlePropertiesMode.GLOBAL, new Vector3D(0,90,0)));
			particleAnimationSet4.addAnimation(new ParticleRotationalVelocityNode(ParticlePropertiesMode.GLOBAL, new Vector3D(0,1,0,25)));
			particleAnimationSet4.addAnimation(new ParticleColorNode(ParticlePropertiesMode.GLOBAL,true,true,false, false,new ColorTransform(1,1,1,0.2), new ColorTransform(1,1,1,0.2)));
			particleAnimationSet4.initParticleFunc = initLightParticleProperties;

			// pan
			panGeoSet.push(pan);
			particleGeometry5 = ParticleGeometryHelper.generateGeometry(panGeoSet);
			particleAnimationSet5 = new ParticleAnimationSet(true, true, false);
			particleAnimationSet5.addAnimation(new ParticleRotationalVelocityNode(ParticlePropertiesMode.GLOBAL, new Vector3D(0,1,0,8)));
			particleAnimationSet5.initParticleFunc = initPanParticleProperties;

		}

		/**
		 * 初始化场景
		 */
		private function initObjects():void
		{
			particleMesh1 = new Mesh(particleGeometry1, matBall);
			particleMesh2 = new Mesh(particleGeometry2, matLight1);
			particleMesh3 = new Mesh(particleGeometry3, matLight2);
			particleMesh4 = new Mesh(particleGeometry4, matLight3);
			particleMesh5 = new Mesh(particleGeometry5, matPan);
			scene.addChild(particleMesh1);
			scene.addChild(particleMesh2);
			scene.addChild(particleMesh3);
			scene.addChild(particleMesh4);
			scene.addChild(particleMesh5);

			animator1 = new ParticleAnimator(particleAnimationSet1);
			animator2 = new ParticleAnimator(particleAnimationSet2);
			animator3 = new ParticleAnimator(particleAnimationSet3);
			animator4 = new ParticleAnimator(particleAnimationSet4);
			animator5 = new ParticleAnimator(particleAnimationSet5);

			particleMesh1.animator = animator1;
			particleMesh2.animator = animator2;
			particleMesh3.animator = animator3;
			particleMesh4.animator = animator4;
			particleMesh5.animator = animator5;

			animator1.start();
			animator2.start();
			animator3.start();
			animator4.start();
			animator5.start();
		}

		/**
		 * 初始化浮动粒子属性
		 */
		private function initBallParticleProperties(properties:ParticleProperties):void
		{
			properties.startTime = Math.random()*4.1;
			properties.duration = 3;
			var degree1:Number = Math.random() * Math.PI ;
			var degree2:Number = Math.random() * Math.PI;
			var r1:Number = Math.random()*15;
			var r2:Number = Math.random()*20 +10;
			properties[ParticlePositionNode.POSITION_VECTOR3D] = new Vector3D(r1*Math.sin(degree1), 0 , r1*Math.cos(degree1));
			properties[ParticleOscillatorNode.OSCILLATOR_VECTOR3D] = new Vector3D(r2*Math.sin(degree2), 0 , r2*Math.cos(degree2), Math.random()*4+2);
			properties[ParticleVelocityNode.VELOCITY_VECTOR3D] =  new Vector3D(0,Math.random()*100,0);
		}

		/**
		 * 初始化光辉属性
		 */
		private function initLightParticleProperties(properties:ParticleProperties):void
		{
			properties.startTime = 1;
			properties.duration = 10;
		}

		/**
		 * 初始化符文转盘属性
		 */
		private function initPanParticleProperties(properties:ParticleProperties):void
		{
			properties.startTime = 1;
			properties.duration = 1;
		}

猛击下载源码

Away3d粒子系统学习与深入I

最近在做基于away3D引擎的粒子编辑器,因此,有了这部分文章作为mark。

Away3D从4.1alpha之后开始加入了粒子系统功能,再次感谢国内牛人LiaoCheng,是他向away3d贡献的这部分功能,大家鼓掌。

简介

所谓粒子系统是表示三维计算机图形学中模拟一些特定的模糊现象的技术,游戏开发中经常使用粒子系统模拟的现象有火、爆炸、烟、水流、火花、落叶、云、雾、雪、尘、流星尾迹或者像发光轨迹这样的抽象视觉效果等等。

Away3D中的粒子系统实现思路

跟大部分游戏的引擎的粒子系统实现思路类似,Away3D的粒子系统遵循以下基本原则:

  1. 每个粒子由带纹理的多边形或几何体构成;
  2. 粒子按一定的规则运动;
  3. 每个粒子有自己的属性,如出生时间、生命周期、位置、速度、加速度、尺寸、颜色、运动轨迹等等;

粒子生成步骤

在Away3D中,由n个粒子组成的粒子特效(为了与广泛的粒子系统区分,我们下面都称为粒子特效)主体,被抽象为一个mesh,而mesh的材质就是给粒子添加的材质,再往mesh中添加animator就组成了一个完整的粒子特效。下面我们就如何构建一个粒子mesh,以及如何为mesh添加动作器展开分析。

构建ParticleGeometry

在上面的的粒子系统基本思路说过:每个粒子由带纹理的多边形或几何体构成,而组成一个完整的粒子特效,则需要n个这样的多边形或几何体,这边我们且称为Geometry Set,我们可以往这个geometry set添加各种Away3D内置的几何体或者外部定义的几何体。出于性能的考虑,建议粒子特效里边最好不要添加太复杂的几何体。

举一个简单的栗子,我们要构建一个向上喷发的小球,就可以如下构建该粒子特效:

var ball:Geometry = new PlaneGeometry(3,3,1,1,false);
var ballGeoSet:Vector.<Geometry> = new Vector.<Geometry>();
// particle ball
for (var i:int = 0; i < 50; i++) {
     ballGeoSet.push(ball);
}

上面的代码,我们添加了包含50个Plane的geometry set,填充这个set可以视情况添加,如果有需要,你也可以同时添加plane、cube、sphere等几何体到这个set里边。准备好geometry set后,我们将使用Away3D提供的particle geometry helper将geometry set转化为最终的ParticleGeometry:

particleGeometry = ParticleGeometryHelper.generateGeometry(ballGeoSet);

这样,我们就完成的一个尚未添加动作的ParticleGeometry实例。

构建ParticleAnimationSet

接下来就是创建粒子动作集合了。类似传统的粒子系统,一个粒子是有其生命周期的,在这个周期中,粒子控制着它所有的属性,位置、轨迹等等。

在Away3D中,一个粒子有关联系统时间的startTime属性,代表粒子出生时间;有可选的duration属性(若没设置duration属性,粒子将无限延续),代表粒子的持续存活时间,还有可选的delay属性(若没设置,将无间隔时间),代表两个重复的粒子周期之间的间隔时间。这些属性基于GPU代码的时间计算,我们必须对要使用的属性进行预定义。

Away3D粒子系统中为粒子动作提供了动作集合ParticleAnimationSet类,构造ParticleAnimationSet需要三个bool参数,第一个参数表示是否使用粒子的duration属性,第二个表示是否不断循环粒子的生命周期,第三个表示是否使用粒子的delay属性,使用则当一个粒子周期执行完成,粒子会消失delay的时间后再进行新的周期。

还是上面的栗子,我们把三个属性都设置为true。

var animationSet:ParticleAnimationSet = new ParticleAnimationSet(true, true, true);
animationSet.initParticleFunc = initParticleParam;

值得注意的是,startTime、duration、delay并不是ParticleAnimationSet的属性,粒子特效中的每个粒子都有自己特有的以上三个属性,在Away3D中,他们被设定为local static属性,我们使用一个函数来定义以上的属性,然后赋给ParticleAnimationSet的initParticleFunc,这样,每个粒子都会调用这个函数一遍。通常,使用ParticleProperties::index这个值设置startTime、duration、delay三个属性,达到每个粒子有不同的以上属性的目的。栗子如下:

private function initParticleParam(prop:ParticleProperties):void {
    prop.startTime = prop.index * 0.005;
    prop.duration = 10;
    prop.delay = 5;
}

以上代码中,我们实现了每5毫秒产生一个粒子,每个粒子延续10秒,然后休眠5秒。

在之后的local static动作属性的设置中,我们也将所用动作的属性设置添加到这个独立的函数中。

Flash和JavaScript互相调用

1.Flash调用JavaScript中的方法
ExternalInterface.call(“js方法名”,“方法入参”);

2.JavaScript调用Flash中的方法
Flash中注册ExternalInterface.addCallback(“供js调用的方法”, flash方法);

例子:

exp.html

<html>
<head>
<title>swfobject demo</title>
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript">
function jsAlert(txt)
{
alert(txt);
}
function flashAlert()
{
var obj = document.getElementById("demo");
obj.flashAlert();
}
</script>
</head>
<body>
<div id="content">
NO Flash Player or NO Support This Flash Player!
</div>
<script type="text/javascript">
var so = new SWFObject("Testjs.swf", "demo", "640", "480", "10", "#000000");
so.addParam("wmode", "transparent");
so.addParam("allowFullScreen", "true");
so.write("content");
</script>
<input type="Button" value="警告" onclick="flashAlert()"/>
</body>
</html>

exp.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="<a href="http://www.adobe.com/2006/mxml" rel="nofollow">http://www.adobe.com/2006/mxml</a>" layout="absolute" creationComplete="init()">
<mx:Script>
<![CDATA[
import mx.controls.Alert;
import flash.external.ExternalInterface;
private function init():void
{
ExternalInterface.addCallback("flashAlert", flashAlertTest);
}
private function doAlert(evt:Event):void
{
ExternalInterface.call("jsAlert", "js Alert");
}
private function flashAlertTest():void
{
Alert.show("flash Alert");
}
]]>
</mx:Script>
<mx:Button x="36" y="97" label="提示" click="doAlert(event)"/>
<mx:Label x="36" y="64" text="Flash调用js" width="135" height="25"/>
</mx:Application>

注意:
1. exp.html, exp.mxml, swfobject.js必须放到web服务器中,直接在本地打开,会出错:SecurityError:Error #2060:安全沙箱冲突:ExternalInterface调用者…

2. ExternalInterface.addCallback(“flashAlert”, flashAlertTest); 必须包含在方法中,然后触发它;否则,会出错:1120:访问的属性flashAlertTest未定义。
如:

private function init():void
{
ExternalInterface.addCallback("flashAlert", flashAlertTest);
}

init();

OpenGL常用命令备忘录(Part B)

3.glPixelStore

像glPixelStorei(GL_PACK_ALIGNMENT, 1)这样的调用,通常会用于像素传输(PACK/UNPACK)的场合。尤其是导入纹理(glTexImage2D)的时候:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(,,,, &pixelData);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

很明显地,它是在改变某个状态量,然后再Restore回来。——为什么是状态?你难道8知道OpenGL就是以状态机不?——什么状态?其实名字已经很直白了,glPixelStore这组函数要改变的是像素的存储格式。

涉及到像素在CPU和GPU上的传输,那就有个存储格式的概念。在本地内存中端像素集合是什么格式?传输到GPU时又是什么格式?格式会是一样么?在glTexImage2D这个函数中,包含两个关于颜色格式的参数,一个是纹理(GPU端,也可以说server端)的,一个是像素数据(程序内存上,也就是client端)的,两者是不一定一样的,哪怕一样也无法代表GPU会像内存那样去存储。或者想象一下,从一张硬盘上的图片提取到内存的像素数据,上传给GPU成为一张纹理,这个“纹理”还会是原来的那种RGBARGBA的一个序列完事么?显然不是的。作为一张纹理,有其纹理ID、WRAP模式、插值模式,指定maipmap时还会有一串各个Level下的map,等等。就纹理的数据来说,本质纹理是边长要满足2的n次方(power of two)的数据集合,这样首先大小上就有可能不一样,另外排列方式也未必就是RGBA的形式。在OpenGL的“解释”中,纹理就是一个“可以被采样的复杂的数据集合”,无论外面世界千变万化,GPU只认纹理作为自己“图像数据结构”,这体现着“规范化”这条世界纽带的伟大之处。

姑且把GPU里面的像素存储格式看做一个未知数,把该存储空间内那批像素看做一堆X。不要深究一堆X究竟是什么样子的,嘛,反正就想象成一堆软绵绵的,或者模糊不清的,打满马赛克的,(哔——)的一样的东西就可以了。与此相比,内存中的像素数据实在太规则规范了!可能源文件各种图片格式,什么bmp、jpg、png甚至dds,但只要你按该格式的算法结构来提取(类似[Bmp文件的结构与基本操作(逐像素印屏版)] ),总可以提取出一列整齐的RGBARGBA(或者RGBRGB什么的,反正很整齐就行了管他呢)的数据堆出来,是可以在程序中实测的东西。

涉及到像素在CPU和GPU上的传输,那就有个传输方向的概念。那就是大家耳濡目染的PACK和UNPACK。嘛,装载和卸载也可以,打包和解压也可以,随你怎么译了。结合上述存储格式的概念:

  • PACK —— 把像素从一堆X的状态转变到规则的状态(把一堆泥土装载进一个花盆,把散散的货物装上货柜,或者把一堆各样的文件打包成一个rar压缩包,等等);
  • UNPACK —— 把像素从规则的状态转变到一堆X的状态(把花盆里的泥倒出来,把货柜中的货物卸载到盐田港,或者解压压缩包,等等)。

我认为这两个概念还是很容易混淆的,所以形象化一点总好点嘛。从本地内存向GPU的传输(UNPACK),包括各种glTexImage、glDrawPixel;从GPU到本地内存的传输(PACK),包括glGetTexImage、glReadPixel等。也正因如此,PBO也有PACK和UNPACK模式的区别。

好像说了好多不相关的事情。嘛,适当也当做延伸。回头来真正看一下glPixelStore吧。它的第一个参数,譬如ALIGNMENT、ROW_LENGTH、IMAGE_HEIGHT等等,都有PACK和UNPACK的两种版本,所以对应的也是上述关于PACH和UNPACK的两类函数。所以对于glTexImage2D,才使用GL_UNPACK_ALIGNMENT的版本。但要说明的是,无论是哪种传输方式,它都是针对本地内存端(client端)上的像素数据的。在上述例子中,它起着补充glTexImage2D中关于传输起点——本地像素集合的格式,的作用。

一般来说,这些本地的数据集合,只要知道其起始位置、大小(width*height)和颜色格式(譬如GL_RGBA等等)、值格式(GL_UNSIGNED_CHAR、GL_FLOAT等等),就能准确地传输。而这些都是需要向glTexImage2D函数(或者上述的其他传输型函数)提供的。但是,这里头也一些细节,其实是需要glPixelStore这个函数来进行设置的。

3.1 GL_UNPACK_ALIGNMENT / GL_PACK_ALIGNMENT

通常,提取一张图像的时候,我们怎么知道一行的数据量呢?这个一行的数据量应该是:width * sizeof(Pixel) ,应对最一般RGBA、各通道各占一个字节的像素结构,width * sizeof(Pixel) = width * 4 * sizeof(byte),是4的整数倍。但是也有时候,我们的像素数据中一行的数据量不一定是4的整数倍(譬如一张RGB的图像、宽度150、各通道各占一个字节的像素结构,一行的数据量就是450个字节)。

另一方面,跟编译器一样,GPU传输时也喜欢4字节对齐,也即是说喜欢对像素数据按4字节存取。所以它更偏向于喜欢每一行的数据量是4的整数倍(按上述,这恰好是比较常见的)。所以为了更高的存取效率,OpenGL默认让像素数据按4字节4字节的方式传输向GPU——但是问题在于,对于行非4字节对齐的像素数据,第一行的最后一次存取的4字节将部分包括第一行的数据部分包括第二行的数据,当然致命的不是在这里,而是在最后一行:存取将很可能会越界。为了防止这样的情况,一是硬性把像素数据延展成4字节对齐的(就像BMP文件的存储方式一样,[Bmp文件的结构与基本操作(逐像素印屏版)] );二是选择绝对会造成4字节对齐的颜色格式或值格式(GL_RGBA啦,或者GL_INT、GL_FLOAT之类);三是以牺牲一些存取效率为代价,去更改OpenGL的字节对齐方式——这就是glPixelStore结合GL_UNPACK_ALIGNMENT / GL_PACK_ALIGNMENT。

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(,,,, &pixelData);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

再次看回这段代码,这时候就明白了:让字节对齐从默认的4字节对齐改成1字节对齐(选择1的话,无论图片本身是怎样都是绝对不会出问题的,嘛,以效率的牺牲为代价),UNPACK像素数据,再把字节对齐方式设置回默认的4字节对齐。至于哪种方式更适合,就看你依据硬件环境限制、麻烦程度等,去选择了。

3.2 GL_UNPACK_ROW_LENGTH/ GL_PACK_ROW_LENGTH 和 
GL_UNPACK_SKIP_ROWS / GL_PACK_SKIP_ROWS 、 GL_UNPACK_SKIP_PIXELS/GL_PACK_SKIP_PIXELS

有的时候,我们把一些小图片拼凑进一张大图片内,这样使用大图片生成的纹理,一来可以使多个原本使用不同的图片作为纹理的同质物件如今能够在同一个Batch内,节省了一些状态切换的开销,二来也容易综合地降低了显存中纹理的总大小。但是,也有些时候,我们需要从原本一张大的图片中,截取图片当中的某一部分作为纹理。要能够做到这样,可以通过预先对图片进行裁剪或者在获得像素数据后,把其中需要的那一部分另外存储到一个Buffer内再交给glTexImage2D之类的函数。而上述这些参数下glPixelStore的使用将帮助我们更好地完成这个目的:

//原图中需要单独提取出来制成纹理的区域
RECT subRect = {{100, 80}, {500, 400}}; //origin.x, origin.y, size.width, size.height

//假设原图的宽度为BaseWidth, 高度为BaseHeight

glPixelStorei(GL_UNPACK_ROW_LENGTH,  BaseWidth);    //指定像素数据中原图的宽度
glPixelStorei(GL_UNPACK_SKIP_ROWS,      subRect. origin.y.); //指定纹理起点偏离原点的高度值
glPixelStorei(GL_UNPACK_SKIP_PIXELS,     subRect. origin.x);  //指定纹理起点偏离原点的宽度值

glTexImage2D(..., subRect.size.width, ubRect.size.height,.. &pixelData);  //使用区域的宽高

glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);

这段代码本身,即使没有注释也很清楚了。注意的是GL_UNPACK_ROW_LENGTH的必要性,因为为了确认区域起点的Offset,就需要把线性数据pixelData上标记起点的“游标”从0移动到OffsetToData = subRect. origin.y * BaseWidth + subRect. origin.x的位置。有了区域纹理原点的在原图数据的位置,以及区域的尺寸,glTexImage2D就可以确定区域纹理生成所需要的信息了。通过glPixelStore的使用,避免了新建Buffer和自己处理图像数据的开销和麻烦了。

说到这里,到底为什么要这样做来提取区域纹理呢?尤其是原图若其他部分都是程序所需要的,那是不是就可以直接通过纹理坐标去切割更好呢?我想到的是一种情况(也可以说我是因为这种情况才注意到glPixelStore的这种用法):如果这块区域纹理需要作重复铺设(wrap mode选择GL_REPEAT)呢?这时候纹理坐标的方法就没用了,因为REPEAT所依据的也是纹理坐标(使用纹理坐标的小数部分进行采样)。这时候就需要上述做法了。(事实上3DSMAX等软件纹理导入的类似区域纹理平铺的功能就能如此实现。)

4.glScissor

我想这个函数也应该很常见才对。裁剪测试啊,当年跟Alpha测试、Depth测试、Stencil测试可以并列哦,而今更是不掉时髦值啊。因为我实在很难想象在Shader里能容易地实现它的功能:裁剪。当然这只是矩形裁剪,但是对于discard掉渲染中不需要的像素真是颇简单粗暴。我使用它最多的是一些二维图片缩略图栏——有时候我们只需要把这些缩略图的显示限制在一个区域里,但又要支持滑动。

glEnable(GL_SCISSOR_TEST);
glScissor(GLint(m_rtThumbRegion.x), GLint(m_rtThumbRegion.y), GLint(m_rtThumbRegion.width), GLint(m_rtThumbRegion.height));
//.....  Render
glDisable(GL_SCISSOR_TEST);

其中,除了启用GL_SCISSOR_TEST外,只要给glScissor指出需要保留显示的区域就可以了。在此区域外的像素依然会被渲染(不会怎么省流水线操作,所以也别指望它附带什么提高效率之类的功能),在下图中,其实左右两侧还是继续渲染其他的图片(或者说,其实这个缩略图栏横跨整个屏幕),但是就在fragment shader之后,它们会被检测到不在该区域内而被discard掉罢。

原文地址:http://www.zwqxin.com/archives/opengl/opengl-api-memorandum-2.html

OpenGL常用命令备忘录(Part A)

1. glCullFace 和 glFrontFace

这两个都是opengl的初级命令,但是其实我都是最近才算得上“用”,以前的话,是因为有一次做billboard广告牌的时候,不剔除面片其中一面的话,很难看出效果成功了没,于是用了一次;最近的话,包括shadow volume中Z-FAIL和Z-PASS都需要正反面分别渲染,shadowmap中在pass 1里剔除正面,在光源视觉下渲染到纹理也用到了,一来减少渲染的”费用”,二来可以提高深度测试的精度(橙书里说的)。以前曾经在哪里看到篇文章说是其实剔除正或反面都提高不了多少效率,首先作者肯定做过测试来了,但是这样算不算武断我也不会说,问过一些搞游戏开发的人,答到“当然能提高效率啦,只不过不可能提高一倍而已”,事实上提高一点点都是很好的说。谁对谁错我也懒得分辨了,因为至少目前我还很少用,而且根据以前尝试的经验决定还是“能不用就不用”,因为正反面判断实在太烦了,有很多时候你用到的是别人弄的模型,而不那么专业的美工会在建模的时候搞乱面片绕序。

所以这组命令除了用在某些特殊场合(实现某些技术所必须),还有就是提高渲染效率的可能性,对某些效果正确性的辅助判断。其中glFrontFace用于指定正面的绕序,默认是“逆时针为正”,即传入参数GL_CCW,可以改成GL_CW顺时针。glCullFace 传入GL_FRONT来剔除正面,或者GL_BACK来剔除背面,之前先得glEnable(GL_CULL_FACE)。

通常情况是这样的:当你想剔除背面的时候,你只需要调用glEnable(GL_CULL_FACE)就可以了,OPENGL状态机会自动按照默认值进行CULL_FACE,默认是glFrontFace(GL_CCW),glCullFace(GL_BACK),当然如果你不想按照默认的来,你就得设置一下了。最后在不再需要剔除的时候调用glDisable(GL_CULL_FACE)。我建议的是“每次开始用,无论是不是默认的你最好都设置一下”,不然你迟些很容易被状态机(保持上一次设置的状态)搞到呕血——除非真的是很简单的demo制作,譬如之前shadow volume中我只是glEnable(GL_CULL_FACE),然后直接glFrontFace(GL_CCW)——它启用剔面功能,指定了“顶点逆时针绕序为正面”,之后的渲染它就会自动剔除那些反面;之后再调用glFrontFace(GL_CW),重新指定为“顶点顺时针绕序为正面”,再渲染一次,注意这时候它同样也是剔除背面(你没改过glCullFace),但是剔除的面就恰恰与前面的相反了。最后在disable前记得先改回glFrontFace(GL_CCW)!否则下次一enable就会按“顺时针绕序为正”了(与常理不一)。(望天!我忘记当时有没设置回来了~)

2.glShadeModel

在opebgl流水线里头,有一个步骤是栅格化(Rasterization),它在顶点组合的几何信息处理后执行,目的是“插值”,vertex shader的varying变量就是在这里被栅格化(/插值),然后再传入fragment shader作象素级别的处理。两顶点之间的象素怎么处理呢?就是靠这两顶点的信息的线性插值。譬如最简单的,颜色,一条线段两顶点A和B的颜色分别为红色和绿色,但是最后“显示颜色”的不可能是顶点,只有象素,因此这两点之间的象素就得按照它们临近的顶点的颜色而获得,越靠近点A的那些象素越红,越靠近点B的那些象素越绿,中间因此呈现渐变效果(一般来说,线段中点有一半红一半绿而成为黄色…)。对于填充的三角形,矩形也是一样的道理,只不过是平面内的线性插值罢了。

说了那么多,其实glShadeModel作用相当于打开/关闭这种功能(栅格化),传入参数GL_FLAT,流水线还是要经过这步骤但相应顶点不被处理,故顶点间的象素的颜色只会参考其中一个点的信息。譬如线段AB上的象素点全是红的或全是绿的——是哪种通常不要紧,因为无论是哪种,出来的结果都会好难看,所以渲染最初(初始化阶段)都会把参数设置成GL_SMOOTH,即启用栅格化功能。当然插值的计算量就上来了…..在渲染不注重效果而只注重速度的时候,譬如我做shadow map的PASS1作场景深度图的离线渲染时,非象素深度的信息根本对我无用,而且象素深度不是插值来的,故关闭栅格化计算,直接glShadeModel(GL_FLAT)再渲染就可以了,之后记得调回glShadeModel(GL_SMOOTH)。

好了,PART 1到此。

原文地址:http://www.zwqxin.com/archives/opengl/opengl-api-memorandum-2.html