write by 九天雁翎(JTianLing) -- www.jtianling.com
讨论新闻组及文件
最新的版本是cocos2d-iphone-0.99.5-rc1.tar.gz
,里面包含cocos2d-mac工程,直接运行,可以看到Mac下Cocos2D运行的效果,happy......
因为暂时还没有好用的模版程序(就像Cocos2D-iphone原来自带的install_template.sh一样),第一反应是相信Cocos2D的强大的开源社区,果然不让人失望.....《HowTo: Create a Cocos2d Mac Project
》一文,完美的描述了从零开始,怎么创建一个Cocos2D for Mac的工程。
然后,最绝的是,更好的解决方案出现了,在论坛的一个thread中
,直接提供了模版下载
.
我越来越依赖于一个这样强大的社区,虽然说自己摸索这些细节也没有太大的问题,也能学到东西,但是,将这些东西交由强大的社区去探索,而自己主力的研究自己真正关注的东西,这样的感觉实在是太好了.......我祝福这些为开源社区做出贡献的先行者........是他们使得本来预计长度与《HowTo: Create a Cocos2d Mac Project
》一样的本文,最后就只需要两个链接而已。
留图:
原创文章作者保留版权 转载请注明原作者 并给出链接
write by 九天雁翎(JTianLing) -- www.jtianling.com
阅读全文....
程序员自虐式杀脑细胞计划
write by 九天雁翎(JTianLing) -- www.jtianling.com
讨论新闻组及文件
在家呆的时间有点久,比较随意,特别是作息,经常通宵看书学习,最近准备重出江湖(没那么夸张,就是再次工作而已......)于是在想调整作息从美国时间到中国时间,但是却比较不适应,感觉白天头晕,四肢乏力,困乏不堪,喝了咖啡都不给力,晚上四眼放光,躺在床上看最高深的天书也不催眠。。。。。。作息的调整看来不是一天两天的事情,常常强行调整结果无功而返。非常无奈,一筹莫展。
然后,碰巧最近帮一个朋友做iphone的摄像头的相关项目,(类似大头贴,即时在视频信息上添加图像)因为apple没有开放类似的API实现功能,只能通过一种很扭曲的方式
来实现。可以说是类似hack,再加上后期在此之上的一些与Cocoa touch相关问题(我对Cocoa Touch的学习并不多),真是那个绞尽脑汁,过程中虽然没有七窍生烟,也是头脑供血长时间过大。完成后,一时有点高中做数学题目的感觉了,呵呵,那久违的感觉啊,突然想到那时候数学老师给我们讲的一个数学多好的故事,大意是某商业人士,虽然从事的工作与数学关系不是非常大,但是平时都以做数学题为乐,无聊的时候就做几道。。。。。。当时听着感觉非常不可思议,没事那么费脑子干什么啊,还嫌脑细胞死的不够多啊,但是当离开了高中以后,发现能够动用脑细胞的时间越来越少,即时是毕业后,用了几乎所有的业余时间来学习,那也主要是一种知识积累型的学习,说实话,还真的不是那么太需要脑力。除了搞了一段时间反汇编,还有点意思,甚至,很多时候,我都是研究怎么更加节省时间脑力,考虑更加快速的完成任务,比如学习更高效的语言(比如简单的事情不用C++而用Python),比如用更方便的UI库(比如抛弃MFC使用Qt),以至于到现在,有种脑细胞旧的不去,新的怎么来的感叹,(我是认为脑袋越用越活的)难怪世上有那么多杀脑细胞的游戏了。这些游戏虽然也是我平时的最爱,但是最近其实玩的也是越来越少了。
再于是乎,有了个杀脑细胞的计划。。。。。。。。简单的说,弄点难点的,需要费脑力的事情来折腾一下,好像很久没有这样折腾过了。。。。。。但是鉴于生活还是得继续,暂时把一些计划定在计划内,也就是平时都可以学习的东西,然后,计划外的,无功利的,我就每天拨两个小时吧。。。。。不干扰正常生活,同时也不让生活过于无聊^^曾经有人说我的想法有些自虐。。。。。暂时这里借用一下。。。。。。
唯一的计划内计划:
图形方面的学习。
从某天拿起一本书OpenGL ES 2.0的书,然后发现看的一头雾水,根本不知道那些光照shader的算法是从哪里冒出来的那天开始,就决心搞定这个领域,然后从很多人推荐的 《Real-Time Rendering》,再到真正所谓的名字就叫图形学的书《计算机图形学》(Computer Graphics with OpenGL Third Edition),知道这个领域还真是杀脑细胞啊。。。。特别是一些算法,要是书上描述的还不是那么好的话,你会看的一头雾水。(比如这里
)而且,这个领域实在太宽广,太深了,学习是没有止境的。。。。。正合我意。。。。。。无聊的时候,看看D3D这边的东西其实也行吧(虽然工作暂时用不上)
一堆的计划外计划:
计划一:
语言的学习。
似乎一直一来都是因为工作需求(比如Lua,Obj-C),或者是学习更加高效,方便的语言(比如Python,Bash),或者纯粹处于兴趣及简单学习的语言(比如Small Basic),而这些,都很少需要费脑子,真正可能稍微难点的东西,或者因为可能平时基本用不上的东西,并没有去学习,想想这样似乎有点过于功利,比如C++的模版元编程,比如Lisp,比如GO,按照以前看到的某篇文章说,作为程序员,要多学习新的语言。。。。。是吧,自从转到iphone上学习Obj-C后,已经有不短的时间了,这段时间都没有学习新的语言,那么,接下来,从C++模版元这个自认为C++最后的角落开始吧,(似乎在大学的时候JAVA就已经比C++应用更加广泛了,但是那时出于本能式的选择了明知道更复杂的C++,工作后,好像还是功利了很多)然后是Lisp,然后是Go。再然后,也许Java,Javascript或者C#都行吧。
计划二:
算法的学习。
当年学习算法都太过于功利,拿起书本就看看对自己有用的东西,到自己感觉差不多了,平时够用了也就停下来了,这样其实漏过了很多有趣的东西。。。。首先弄基本基础的看看,然后搞一本《算法导论》,再考虑是不是弄《计算机编程艺术》系列来看看。这方面估计可以杀掉挺久的时间。
计划三:
数学的学习。
这肯定是一个对我来说无止境的领域。。。。。。。到了这个地步,啥都不说了。。。。从基础到难的,拿本书玩命看贝。。。呵呵,也许一辈子也不用找其它的东西来杀时间了。。。。
其实,总的来说,该计划还是有一定功利性的,毕竟和我的工作多少有些关联,(即使实际工作上能用到的少的可怜)也许这样我才能更加愿意投入时间去弄吧,虽然我也对一些纯粹的杀脑细胞智力题有兴趣,但是暂时也就是看看,没有准备花太多时间研究。(真有这样的人的......比如这里
)
原创文章作者保留版权 转载请注明原作者 并给出链接
write by 九天雁翎(JTianLing) -- www.jtianling.com
阅读全文....
游戏设计的秘密——翻译GDC2010 blizzard的一个演讲
转自:http://blog.sina.com.cn/s/blog_62c6329f0100hmia.html
翻译了一篇blizzard在GDC2010上的演讲。感谢cyndi的校正和粽子的润色。有不对的地方,还请大家指出。
游戏设计的秘密
理念一:游戏性第一。有些公司把技术看得比游戏性还重要,这是毫无意义的。在暴雪,包括美术、策划、程序每个人都很关注游戏性,这并非策划的专利。——无论策划能想出了多么牛的方案,大家依然从中找出瑕疵。
让我们来看一个例子:在魔兽争霸3
中,我们有一个设定是这样的:只有暗夜精灵男性才能成为德鲁伊。但换到了魔兽世界中,假如依然只有暗夜精灵,依然只有男性才能成为德鲁伊,这太没意思了!所以,我们修改了这条设定。
理念二:易于上手,难于精通。我们把这条格言稍微地进行了修改:易于上手,几乎不可能精通。
游戏要做到极致才能让玩家为之疯狂,
从而投入大量的游戏时间。
这些年来,暴雪都很关注多人游戏。我无法明白许多开发人员愿意花费大量的精力在不到10
小时的单人游戏上,却只是在完工前草草地补上多人部分,这简直不可理喻!相对来说,
多人游戏有明显的优势和潜质能吸引玩家投入更长的游戏时间。
在星际争霸2
的开发过程中,整个研发组用了2
年时间制作多人模式后,才开始制作单人战役。值得一提的是:在多人部分制作完毕后,调整单人战役的平衡要容易得多。
理念三:带入感。游戏带给玩家的体验能完美地符合玩家期望——如吉他英雄一般演奏,像奎托斯一般弑神,这就是强大的带入感。
在这一点上,原版的星际争霸做得并不好,特别是在英雄单位上。按道理说,英雄应该是战场上非常强大的单位,甚至能左右战场的局势。但在游戏中,英雄往往是很脆弱的,特别是在超过50
个单位作战的时候就更加明显了。因此,玩家不再让他们的英雄加入战斗,而只是把它们藏在自己的基地之中。我们在魔兽争霸3
中解决了这个问题——
缩小了战斗规模,让有复生能力的传奇英雄们成为战场上真正的主宰者。正如我们设计的初衷一样,玩家们也乐于使用这些强大的英雄单位。
值得注意的是:我们应该让单位在
单独存在时,让它有一种无懈可击的感觉,但当众多单位同时存在的时候,却又有相生相克的关系,从而维持游戏的平衡性。其中有一种方法可以解决这个问题——加强单位与单位之间的差异:如果你提升了某些单位的攻击力,那么就该相应提升另外一些单位的防御力。
无论是在背景设定、游戏性还是故事本身的延续,让游戏中的事物有史诗般的感觉是百利而无一害的。以wow
中龙的模型为例,最初的模型只有玩家角色5
倍大小而已。我让研发做了一个GM
技能,可以使任意目标放大或是缩小10%
。有一次,在一个孤岛上,我对着一条龙施放“放大术”近20
次才满意的停手。至今为止,我都还没有对任何单位使用过“缩小术”。
过去发生的某些事,就当它发生在10000
年前好了,何必纠结于8
尺身高的人是否存在的问题?大胆的做就好了。有了这样的念头,我们的故事和设定就不会有太多约束。
理念四:少而精。我们要提炼游戏中必不可少的元素,最精彩的部分而不是放出一大堆复杂的功能和玩法。在这一点上,魔兽世界的交通系统做得并不好,而星际争霸的单位特色却非常成功。
理念五:要玩家去体验,而不要平铺直叙
。这个理念来自一个古老的谚语——要秀出来,不要说出来!游戏诉说故事的首要方法是让玩家去体验、去感受,而非文字、声音、动画。
在这一点上,我们曾经在暗黑2
中栽过跟头。游戏中的任务通常会毫无目的地给出大段描述,简单的说:去某某地方,干掉某某!相比而言,魔兽争霸3
的“灭绝!斯坦索姆”任务就做得很不错:整个村庄都被瘟疫所侵袭,不久村民将成为新的僵尸加入恐惧魔王的势力。而你必须带领部队,在他们变成僵尸之前,把村民们全部干掉!
理念六:激励。激励的作用往往要大于惩罚,但设计师们总是很容易先想到如何去惩罚玩家。在wow
的beta
阶段,我们有一个设计:一旦玩家在线时间过长,他获得经验值的效率将降低——这通常让玩家很郁闷。
在无数的批评反馈中,我们修改了设计:以前玩家在线时间过长,只会获得50%
的经验值,现在我们让他获得100%
!而正常时间内,我们让他获得200%
的经验。与此同时,我们把人物升级经验加了1
倍——
玩家获得经验值的效率并没有改变,我们仅仅是在思维和表达方式上化贬为褒,就再也没人抱怨过了。
理念七:操作性。有时候,我们为了操作性得放弃一些听起来很酷的点子。最初,设计师们希望坐骑系统具有塞尔达风格——当召唤时,玩家的坐骑会从地平线那端
跑到玩家跟前。但最后,我们采用了现在的方式——“噗”的一声,玩家就召唤出了坐骑。这是因为我们发现,坐骑出现的时间、地点对玩家来说更重要。
理念八:调整。游戏调整是很经常的事情,但重要的是得想清楚:这些修改是为了哪些用户,以及为什么这么改。然而有些设计师,在还没想清楚这些问题之前,就盲目的修改,这是非常失败的。
理念九:切忌闭门造车。设计师们如果能在早期就跟大家分享、沟通自己的设计案,那么他能得到许多重要的反馈,从而做出改进。但如果一个设计师,闷头做了很久,就会很容易变得情绪化。最后,他向大家宣布设计的时候,更像在寻求别人的肯定而不是什么建议。
理念十:产品化。我们会在游戏之初就开始坚持游戏产品化工作,而不是等到游戏基本定型后才开始——在游戏还没有可玩版本的时候我们会做,甚至这个概念还只是出现在白板之上的时候,我们就已经开始产品化了。
我们有一个“突击队”,他们由公司其他项目的同事和一些外来人员组成。他们的任务是给我们游戏的提出反馈意见。他们像新鲜血液一样,总能带来新的东西。产品化
也起着至关重要的作用,暴雪是绝对不会在未完成产品化之前发售游戏的。
p.s.
:所有的这些理念都是为了做一款杰出的游戏而存在的,离开这个目标,它们都是毫无意义的。你们应该总结自己的设计理念,只有让游戏符合自己的价值观,才能体验到游戏设计的快乐。
comment:即使作为程序,我也觉的很有意思,理念一更加是非常认同。
阅读全文....
关于UI的选择,看过一篇比较有意思并且全面的文章,但是里面谈论到的是只做网游时,而对于iPhone这种硬件限制远远多于PC的环境来说(特别是内存紧张),使用Ogre本身就是一种很奢侈的事情了,在UI部分消耗有很多内存,那就几乎没有办法去创建稍微复杂点的场景了,(我尝试过Ogre+Bullet+OgreBullet,在载入一个不复杂的场景,仅包含几十个Box的时候,我的touch 3代就会报内存警告,并且强制程序退出了)所以,在选择UI时,有更多不同的考虑,当然,那些在PC下都有效率问题的UI更加是不用考虑了。
首先说需求,很明显做iPhone游戏,特别是比较简单的iPhone游戏,对UI的需求比做网游要求还是少的,对于我来说,按钮与文字的显示就已经能够包含90%以上的需求了,要是按钮还能够多点定制的灵活性,文字还能够显示中文,然后还有个进度条(用于loading)就完美了,没有其他更多的需求。唯一
知道需求后,就会发现,CEGUI
这个成熟并且有足够灵活性的UI是很多公司开发网游的最爱,但是还是太庞大,太复杂了一些。再加上这里有个人总结过iphone下UI的选择
,谈到了CEGUI在iPhone下的问题,所以不予考虑。(虽然这个我以前用过,可能上手会快一些)我认为在前面提及文章中作者最后选择的MyGUI可能都复杂了,加上在网上找了找相关的信息,发现iPhone下也会有一些问题
。并且,我发现官方版本的MyGUI
甚至没有MacOS版本。这个问题太严重了,我还是不想再做吃螃蟹的人了,我需要的东西是那么简单。。。。于是,我决定尝试Ogre的内置UI,(可惜文档太少)假如内置UI不能解决问题,再去考虑其他的UI。
Ogre内置的UI
说实话,Ogre其实没有内置的UI。。。。。。事实上,demo中那些控件的实现其实都是直接建立在demo工程中的,也就是说,这些UI根本就不是Ogre SDK的一部分,随时有可能被抛弃或者更改,也没有人保证这些代码的稳定性,(所以类的命名上,有Sdk这样特殊的前缀)但是,如上所述,实在是没有太多更好的选择,好在这些代码还是比较简洁(因为Ogre就没有想做一个庞大的UI系统,仅仅是想在demo中有个UI可以使用),就算直接拷贝使用,然后自己维护,都还是可以接受的方案。这些代码都在demo的SDKTrays.h文件中,都在OgreBites命名空间下。
另外,因为这些代码都不是Ogre本身SDK的一部分,(只是demo的一部分)所以,甚至连API文档
都没有,(只有overlay这一层的文档)更别说详细的说明文档了,没有文档,那么源代码就是最好的文档。。。。。。。。
使用
这里的使用以Ogre的1.7.2的SDK为起点,不涉及在Windows中的编译等问题。
Sdk相关的文件有两个,(在SDK的包中)分别在目录
OgreSDK_vc9_v1-7-2/Samples/Common/include
OgreSDK_vc9_v1-7-2/include/OGRE
中,但是因为考虑到此文件可能需要自己修改,建议复制一份,改个名字,然后自己包含到工程中使用。
首先,创建你自己的SdkTrayManager类型的对象,我推荐弄成单件。在Ogre VC9 AppWizard
中的BaseApplication中已经有成员变量mTrayManager了,(standard application模板)并且恰当的初始化了,拿来直接用就可以。
具体控件的使用实在不能再简单了。。。。。
比如label:
mTrayMgr->createLabel(TL_TOPLEFT, "HelloWorldLabel", "HelloWorld");
第一个参数是位置,第二个参数是label对象的名字,第三个参数是label显示的文字。
比如按钮:
创建:
mTrayMgr->createButton(TL_TOPLEFT, "Quit", "Quit");
第一个参数是位置,第二个参数是按钮对象的名字,第三个参数是按钮上显示的文字。
查询状态:(在你的循环逻辑中调用啊,比如frameRenderingQueued)
Button* bt = (Button*)mTrayMgr->getWidget("Quit");
if (bt->getState() == BS_DOWN) {
// put your codes here
}
比如进度条:
创建:
mTrayMgr->createProgressBar(TL_TOPLEFT, "LoadingControl", "Loading", 200.0, 250.0);
最后两个参数一个表示进度条的长度,一个表示注释文字的长度,会在caption的后面建立一个让文字显示非常浅的框。。。。。但是,又没有一个comment文字参数的直接传入,所以你只能靠caption文字自己来设想了。。。。这个设计倒是可以理解,比如初始化读取文件的时候,前面显示标题,后面显示正在读取的文件,但是就是为啥没有一个comment文字参数呢?不理解。而且还去不掉。。。。。
设置进度:(半分比,以0到1的浮点数来表示)
ProgressBar* pb = mTrayMgr->createProgressBar(TL_TOPLEFT, "LoadingControl", "Loading", 200.0, 250.0);
pb->setProgress(.5); // 50%
控件的使用是比较简单的,但是有几个需要注意的地方:
1.假如需要鼠标指针的话(iphone自然不需要了):
mTrayMgr->showCursor();
2.通过上面的方式控件创建后,会发现caption显示不出来,因为字体还没有载入,需要下列代码:
Ogre::FontManager::getSingleton().getByName("SdkTrays/Caption")->load();
依次创建上述三个控件的效果:
点击I Will Quit按钮,可以正常退出。
有些意思的是同一个位置的控件会自动排列,这点就像qt等UI中的layout,这个非常方便,因为不需要手动指定绝对坐标位置。(比起古老的MFC而言)但是,好像这个layout(先这么称呼吧)只能垂直排列,不能设定为水平排列。
待解决问题:
1.label在文字较长的时候是不会自动扩展大小的(按钮会)。
2.这些UI还是太简单了,起码的自定义图片按钮都不能做到,别说啥主题切换(换肤)功能了,要是真用这些原始UI做一个游戏,会被别人当作Ogre的demo处理的。。。。-_-!即使通过直接替换这些控件的图片,游戏中一个控件也只能有一个样子了,可能会稍微单调一点。。。。。
3.ProgressBar不知道怎么才能方便的使用,现在的设计实在有问题的太离谱了。
还好,有了个简单的基础了,这些功能就慢慢自己加吧,说不定哪天提供给有同样需求的兄弟下载。
另外,loading进度条我看到sdkManager里面有个showLoadingBar函数,还没有试用,好用的话,也许可以直接用。
实现
Ogre对UI的支持在Overlay这一层(有个OverlayManager),demo的UI就是从这里开始的。
事件的响应全部通过下列listener的回调,你也能看出大概此套UI关注哪些事件。
/*
=============================================================================
| Listener class for responding to tray events.
=============================================================================
*/
class
SdkTrayListener
{
public
:
virtual
~SdkTrayListener() {}
virtual
void
buttonHit(Button* button) {}
virtual
void
itemSelected(SelectMenu* menu) {}
virtual
void
labelHit(Label* label) {}
virtual
void
sliderMoved(Slider* slider) {}
virtual
void
checkBoxToggled(CheckBox* box) {}
virtual
void
okDialogClosed(const
Ogre::DisplayString& message) {}
virtual
void
yesNoDialogClosed(const
Ogre::DisplayString& question, bool
yesHit) {}
};
SdkTrayManager
是从此listener继承来的:
/*
=============================================================================
| Main class to manage a cursor, backdrop, trays and widgets.
=============================================================================
*/
class
SdkTrayManager : public
SdkTrayListener, public
Ogre::ResourceGroupListener
虽然SdkTrayManager的主要功能可能与OverlayManager类似,但是因为OverlayManager是个单件,(所以实际也就决定了不方便继承使用)所以,实际每次用到的时候直接获取此类的对象然后使用即可。
事件的响应部分,和CEGUI等UI类似,利用了一个injectXX接口,这里就不多说了。
然后,所有的控件有个基类:Widget
此类中有3个成员变量:
Ogre::OverlayElement* mElement;
TrayLocation mTrayLoc;
SdkTrayListener* mListener;
OverlayElement自然是与OverlayManager配合使用的,TrayLocation用于表示位置,Listener就是前面那个,这里也看出一些问题,即使是个按钮,也会有个这样庞大的listener。。。。这个可能与C++的没有方便的回调或者协议使用方式有关,假如Ogre愿意为此使用信号或者消息模式,就不需要这样,不然的话,为每个类型的控件建立单独的listener类是最好的办法,Ogre的这个UI又仅仅是设计给demo用的,不想弄那么复杂,所以就用了这种非常丑陋的用法。于是,你甚至可以在一个按钮控件中等待按钮根本不会响应的事件。
具体的控件,以按钮为例:
class Button : public Widget
按钮继承自Widget,有下列成员变量:
ButtonState mState;
Ogre::BorderPanelOverlayElement* mBP;
Ogre::TextAreaOverlayElement* mTextArea;
bool mFitToContents;
mState自然表示按钮的状态:
enum ButtonState // enumerator values for button states
{
BS_UP,
BS_OVER,
BS_DOWN
};
BorderPanelOverlayElement提供了一个有边框的Overlay元素。(因为Widget已经包含了一个OverlayElement,可以看出此UI的设计上还是有些问题,可能毕竟是仅仅设计给Demo用的东西吧)
TextAreaOverlayElement提供一个文字Overlay元素。
按钮就是一个有边框的,可选显示文字,能够反映点击状态的控件,上述成员变量可以完全的诠释按钮。。。。。。
mFitToContents变量就是使得按钮可以自动扩展适应文字宽度的值,当手动设定按钮宽度时为false,不然就是true。label中就是没有这样的配置,然后就不能自适应。
实现上,在setCaption中会有
if (mFitToContents) mElement->setWidth(getCaptionWidth(caption, mTextArea) + mElement->getHeight() - 12);
这个操作。label没有。
稍微浏览了一下源码,基本上没有太多内容。。。。。。。
原创文章作者保留版权 转载请注明原作者 并给出链接
write by 九天雁翎(JTianLing) -- www.jtianling.com
阅读全文....
新版本的Ogre(1.7.2)彻底解决了前面版本关于iOS4的一些问题,但是用SDK编译release版本可以做到WOB,但是假如需要debug版本的Ogre的话,还是得自己编译,用CMake 2.8-3版本,添加OGRE_BUILD_PLATFORM_IPHONE的bool变量,然后勾选ogles,去掉ogl,配置一下freetype库(这最隐蔽,不然会得到一些链接错误),生成后在生成的工程目录运行> ../SDK/iPhone/fix_linker_paths.sh,还特别注意一下需要使用SDK4.1版本编译就行了(老版本似乎会缺少一些新的OGLES扩展)基本上还是比较简单。只是我尝试在ipad上运行时,速度还勉强,但是触摸响应极慢。。。。。。。。难道这就是传说中的display link的触摸响应延迟问题?(Ogre 1.7.2的what't news中提到了一点)
ipad下的截图:
原创文章作者保留版权 转载请注明原作者 并给出链接
write by 九天雁翎(JTianLing) -- www.jtianling.com
阅读全文....
直线的光栅化算法
write by 九天雁翎(JTianLing) -- www.jtianling.com
讨论新闻组及文件
看"参考1"碰到的第一个算法,有点意思,总结一下,特别是因为"参考1"没有找到随书的源码下载(Pearson的源码都要教师申请才能下载),自己写点源码,也算是桩乐事。
因为在我们画图的时候,都是以vertex为基础的,所以算法都是以直线(实际是数学中的线段)的起点(x0,y0)加终点(xEnd,yEnd)作为输入来绘制直线的。
基础知识
因为这是第一次提到图形学的算法,这里弄一些预备知识,Glut,OpenGL啥的我倒是不多说了,只是最近我习惯了Mac,所以源代码工程都是XCode的,并且OpenGL包含的目录可能与Win32下不同,这点需要特别注意。
另外,很明显的,OpenGL本身就可以直接绘制直线了。。。。。。所以这里自然不能利用OpenGL的相关功能,不然也没有啥好学的了,更加牵涉不到所谓的"算法",直接看"参考4"即可。
所以,这里只使用一个功能,那就是setPixel函数,此函数也利用OpenGL完成,特别的,为了适应OpenGL ES,我没有如一般的书籍,使用glBegin,glEnd API。
setPixel函数实现如下:
void setPixel(int x, int y) {
GLint vertices[] = {x, y};
glVertexPointer(2, GL_INT, 0, vertices);
glDrawArrays(GL_POINTS, 0, 1);
}
另外,关于绘制直线的环境,这里我为了做到真实显示像素与窗口中坐标点的一一对应,所以如下设置:
static int gw, gh;
void init2d(int w, int h) {
gw = w;
gh = h;
/* attributes */
glClearColor(1.0, 1.0, 1.0, 1.0); /* white background */
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D( - w / 2, w / 2, - h / 2, h / 2);
glMatrixMode(GL_MODELVIEW);
glEnableClientState(GL_VERTEX_ARRAY);
}
并且,还是维持OpenGL原点在中心的特点,同时也方便展示算法在4个象限中的不同。
具体的Glut main函数代码那就非常简单了,这里也贴一下:
int main (int argc, char *argv[]) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
glutInitWindowSize(640, 480);
glutInitWindowPosition(200, 200);
glutCreateWindow("draw lines");
init2d(640, 480);
glutDisplayFunc(display);
glutMainLoop();
return 0;
}
实际我们现在需要关心的也就是display回调函数了。(再次提醒,不明白OpenGL 或者 Glut的,请自行参考本文的"参考4")
利用直线方程
首先,我们知道输入了,
void drawLine(int x0, int y0, int xEnd, int yEnd);
最容易想到的就是直接利用高中学到的直线方程了,这里有2个已知点了,那么直线的点斜式方程就非常容易求出来了。
国外的教科书一般如下:
直线点斜式方程:
(式1)
斜率
截距
将m,b带入式1,对于直线上任意一点,知道x坐标,那么
(式2)
当然,假如更加直观的用直线两点式方程来算y,会更加直接。
通过
直线两点式方程:
(式3)
可以直接求得式2.
可以得出初步的通过直线方程来画直线的算法:
1.计算斜率m
2.从x0开始,每次让x坐标递增1,求得直线的y坐标。
我们可以得到下面的代码:
void drawLineWithEquation(int x0, int y0, int xEnd, int yEnd) {
// slope, notice the difference between the true C/C++ code and the math equation
float m = (float)(yEnd - y0) / (xEnd - x0);
// y = (x - x0) * m + y0;
for (int x = x0; x <= xEnd; ++x) {
int y = lroundf(( x - x0 ) * m + y0);
setPixel(x, y);
}
}
需要特别注意的是,对于强类型语言的变量计算时与数学公式的区别所在,都用整数计算那就会发生严重的精度丢失问题。
我们绘制如下直线:
drawLineWithEquation(0, 0, 300, 50);
drawLineWithEquation(0, 0, 300, 100);
drawLineWithEquation(0, 0, 300, 150);
drawLineWithEquation(0, 0, 300, 300);
drawLineWithEquation(0, 0, 150, 300);
drawLineWithEquation(0, 0, 100, 300);
drawLineWithEquation(0, 0, 50, 300);
对于一些直线,我们已经可以看到漂亮的结果了。。。。。。。但是,很明显的,当X变化较小,而Y变化较大时,(也就是斜率m>1时)效果非常不好,显示出来的效果可以很明显的看到是一系列离散的点。
原因其实想象就比较容易理解了,因为我们递增的总是X坐标,只需要通过判断斜率,适时的递增Y坐标即可。
那么,就需要根据已知的Y坐标求直线上的点的X坐标了,通过前面的式3,可以直接求得:
(式4)
改进后的代码如下:
void drawLineWithEquation(int x0, int y0, int xEnd, int yEnd) {
// slope, notice the difference between the true C/C++ code and the math equation
float m = (float)(yEnd - y0) / (xEnd - x0);
float mr = 1 / m;
if (m <= 1) {
// when m <= 1: y = (x - x0) * m + y0
for (int x = x0; x <= xEnd; ++x) {
int y = lroundf(( x - x0 ) * m + y0);
setPixel(x, y);
}
}
else {
// when m > 1: x = (y-y0) / m + x0
for (int y = y0; y <= yEnd; ++y) {
int x = lroundf(( y - y0 ) * mr + x0);
setPixel(x, y);
}
}
}
这样效果就好了:
我们一直都用递增来完成直线方程,这里就还是有问题了,因为有可能直线斜率是<0的,(即起点高,终点低),那么我们还需要判断,并通过递减来解决问题。但是实际这样会使得代码进一步复杂化,我们预先通过x0与xEnd,y0与yEnd之间的比较,在的确xEnd,yEnd比x0,y0小的时候,对应的交换xEnd与x0, yEnd与y0即可。从逻辑上来看,因为绘制直线的时候没有方向的概念,那么一条x,y坐标递减的直线总是能看做反方向递增的直线。(相当于反过来画此直线)这样就能在不大量增加代码复杂度的时候确保,总是通过递增能够解决此问题。
但是,假如按上面的判断方式,将有很多种情况,分别是dx,dy与0的比较,以及m与0,1,-1的比较,也就是4*2=8个分支。代码会类似下面这样:
void drawLineWithEquation(int x0, int y0, int xEnd, int yEnd) {
// slope, notice the difference between the true C/C++ code and the math equation
int dx = xEnd - x0;
int dy = yEnd - y0;
float m = (float)dy / dx;
float mr = 1 / m;
if (m <-1) {
if (dy < 0) {
swap(y0, yEnd);
swap(x0, xEnd);
}
for (int y = y0; y <= yEnd; ++y) {
int x = lroundf(( y - y0 ) * mr + x0);
setPixel(x, y);
}
}
else if (m < 0) {
if (dx < 0) {
swap(x0, xEnd);
swap(y0, yEnd);
}
for (int x = x0; x <= xEnd; ++x) {
int y = lroundf(( x - x0 ) * m + y0);
setPixel(x, y);
}
}
else if (m <= 1) {
if (dx < 0) {
swap(x0, xEnd);
swap(y0, yEnd);
}
// when m <= 1: y = (x - x0) * m + y0
for (int x = x0; x <= xEnd; ++x) {
int y = lroundf(( x - x0 ) * m + y0);
setPixel(x, y);
}
}
else {
if (dy < 0) {
swap(y0, yEnd);
swap(x0, xEnd);
}
// when m > 1: x = (y-y0) / m + x0
for (int y = y0; y <= yEnd; ++y) {
int x = lroundf(( y - y0 ) * mr + x0);
setPixel(x, y);
}
}
}
这样又太麻烦了。事实上,问题总是归结与两种情况,一种是X变化大,一种是Y变化大,因此可以仅进行dx,dy的大小判断以减少代码的分支,也就是判断fabs(dx)及fabs(dy)的大小关系。如此,可以得到较为简单的代码:
void drawLineWithEquation(int x0, int y0, int xEnd, int yEnd) {
// slope, notice the difference between the true C/C++ code and the math equation
int dx = xEnd - x0;
int dy = yEnd - y0;
float m = (float)dy / dx;
float mr = 1 / m;
if ( abs(dx) >= abs(dy) ) {
if (dx < 0) {
swap(x0, xEnd);
swap(y0, yEnd);
}
for (int x = x0; x <= xEnd; ++x) {
int y = lroundf( ( x - x0 ) * m + y0);
setPixel(x, y);
}
}
else {
if (dy < 0) {
swap(y0, yEnd);
swap(x0, xEnd);
}
// when m > 1: x = (y-y0) / m + x0
for (int y = y0; y <= yEnd; ++y) {
int x = lroundf(( y - y0 ) * mr + x0);
setPixel(x, y);
}
}
}
作为完整性测试,这里以原点为圆心,生成一个圆,直线的另一个端点总是落在圆周上,以此可以检验4个象限的绘制。
圆的参数方程:
(式5)
然后以15度递增(15度非常有意思,递增时会绘制较为特殊的45,90,180...等较为特殊的角度)以此式完成的测试代码如下:
int r = 300;
for (float theta = PI / 12; theta <= 2 * PI ; theta += PI / 12) {
int x = r * cos(theta);
int y = r * sin(theta);
drawLineWithEquation(0, 0, x, y);
}
最后绘制的效果如下,显示效果基本完美:
DDA算法(digital differential analyzer)
个人感觉从直线的方程绘制直线的方法想到DDA算法还算比较自然。
观察一下上面通过方程绘制直线的代码,有个地方会觉得明显还可以进一步优化,那就是swap函数的存在。因为在上面的算法种我们总是希望通过递增来解决问题,而事实上,递减又未尝不可,假如可以接受递减,那么就不需要swap的操作了,而同时,又不希望开新的分支来区分递增递减,那么很自然的可以想到,直接将x,y的递增递减量算出来,负的就递减,正的就递增,而我们只需要用+来处理,根本不用关心其符号,因为都正好符合要求。同时,因为x0与xEnd,y0与yEnd的大小判断此时已经不方便作为循环结束的标识(不然又得分支),所以同时提出循环次数,此时循环次数就等于max(fabs(dx),fabs(dy)),就能很自然的得到DDA算法。(这种描述是我自己从代码的优化角度推导出DDA算法的过程,需要更加严格数学推导及描述的,可以参考"参考1“的DDA算法部分。
通过这种思想,可以得出DDA算法的画线代码:
void drawLineWithDDA(int x0, int y0, int xEnd, int yEnd) {
// slope, notice the difference between the true C/C++ code and the math equation
int dx = xEnd - x0;
int dy = yEnd - y0;
float x = x0;
float y = y0;
int steps = max(abs(dx), abs(dy));
float xIncrement = float(dx) / steps;
float yIncrement = float(dy) / steps;
setPixel(x, y);
for (int i = 0; i <= steps; ++i) {
x += xIncrement;
y += yIncrement;
setPixel(lroundf(x), lroundf(y));
}
}
效果不变,这里就不截图了。同时,可以看到,这个算法提炼了一些概念以后,是要比前一个算法效率高的,用每次绘制直线只需要一次的除法替代了循环中的乘法。
简单的说,该算法的效率提升来自从原来直线方程算法的连续计算到离散运算。
但是此算法用到了浮点运算,并且最后有浮点到整数的取整操作,这些操作使得算法效率还有改进的空间。
Bresenham算法
Bresenham算法是由Bresenham 1962年发明,1965年发表,(见WIKI
,"参考3"认为发表的时间就是发明的时间,可能有误)鬼才知道,为啥在那个年代,还在用绘图仪画线时,Bresenham这种牛人是怎么想到这种扭曲,思维跳跃的算法的。突然想到某地方看到一个笑话,话说某个中学生特别讨厌牛顿,莱布尼茨等人。。。。。因为他们不弄出那么一堆奇怪的定理和算法,那么学习会容易的多。。。。呵呵
Bresenham算法是现在硬件和软件光栅化的标准算法。(见"参考2"352面)相比DDA算法的好处在于完全避免了浮点运算,以及DDA算法因为有浮点运算而带来的取整运算,因此更加高效。
不过算法的发明的思维简直是无敌了,pf啊pf......简单的说,该算法简单的说,将DDA的严谨的离散运算都简化成上一个点,下一个点的逻辑判断了。。。。牛啊
该算法书中描述得很多,"参考1"描述的最多,有严格的数学推导,但是不好理解,因为太数学化,没有很好的描述偏移error的概念,"参考2"较为简略,而且有点奇怪的用i+1/2这样的像素点位置,单独看估计看不懂,"参考3"的思路是从纯逻辑的思路来看的,没有数学推导,所以不太严谨,但是并没有较为完美的描述,于是,我找到了作者原来的论文,进行参考。才发现,"参考1"的推导比作者原来表述的还要详细,原论文以证明为主,但是同时要更加严谨一些,比如在"参考1"中直接提出了dUpper和dLower的概念,并以此为根据来判断像素点,事实上"参考6"是先提出了到直线的距离,然后根据(dLower-dUpper)与先前的点到直线的距离的差符号一致,才开始转入对dUpper和dLower的使用,同时,"参考6"也完整的描述了所有区段的算法使用。
另外,在查找资料的过程中,发现很多奇怪的现象,比如,为了简化Bresenham算法的推导,很多资料完全放弃了数学公式的推导,然后仅仅凭空的添加进0.5这个值,意思就是直线在k+1点过了下一个像素位置一半,那么就选择y+1点,不然就选择y点,如WIKI
,《The Bresenham Line-Drawing Algorithm
》,然后再煞有介事的提出进一步的优化方案,以消除这个浮点数。实际上,看过原论文以后,我发现这些资料简直就是误人子弟,因为在作者提出算法的时候就从来没有用过啥0.5这个值,而且作者在描述此算法的优点时,就明确的提到不需要浮点运算。也就是说,前面的那些误人子弟的推导,推导出来的东西,根本就不能称作Bresenham算法,但是偏偏广为流传。。。。。。。。。。。
因为该算法的推导较为复杂,而"参考1"已经写的很详细了,(加上原论文)我也懒得在此重新写一遍了。没有此书的人已经不必要再继续看了,这里在此补充一些"参考1"没有提到的信息,已经解决一些"参考1"的问题。
1.推导的严谨程度来说,直接的初始条件是从点yk及yk+1到直线距离,哪个近,选择哪个点,因为此距离差与文中提及的dLower和dUpper差一致,所以才可以转为判断pk。
2.所谓重新安排等式(莫名其妙),其实就是在式(3.13)右边乘以
,以形成Bresenham对决策参数的定义式。
3.你怎么样都不能在书中提及的条件下,将x0,y0,以及
带入式(3.14)计算得到
,这里还需要用到原作者论文中提到过的假设,那就是
,
,因为作者在之前有坐标系平移的一步,将新坐标系的原点移到(x0,y0)点,估计“参考1”的作者也不是完全理解算法,所以就马虎的忽悠一下企图过关了。
另外提醒大家,网络的资料有的时候还是谨慎着点看,还是较为权威的教材最靠谱,(虽然也不一定没有问题)学习算法时,第一次提出该算法的论文实在是最好的参考资料。因为"参考1"有上述我提到的3个问题,所以一开始我跟着学习总是很纳闷,结合了很多资料来看,又碰到一堆误人子弟的资料,还是原始资料最靠谱。。。。。。。别人可以理解这个算法,但是有人能够理解的比发明这个算法的人要透彻的吗?
参考:
1.计算机图形学, Computer Graphics with OpenGL, Third Edition, Donald Hearn, M.Pauline Baker著, 蔡世杰等译, 电子工业出版社
2.交互式计算机图形学--基于OpenGL的自顶向下方法(第五版),Interactive Computer Graphics -- A Top-Down Approach Using OpenGL, Fifth Edition 英文版 Edward Angel 著, 电子工业出版社
3.Windows游戏编程大师技巧(第二版), Tricks of the Windows Game Programming Gurus Second Edition, Andre Lamothe著, 沙鹰 译, 中国电力出版社
4.OpenGL 编程指南(第六版) OpenGL Programming Guide, OpenGL Archiecture Review Board, Dave Shreiner等著, 徐波 译, 机械工业出版社
5.Princeton University
开放课程信息
6.Algorithm for computer control of a digital plotter
,J. E. Bresenham的关于Bresenham算法的原始论文,IBM Systems Journal 1965
原创文章作者保留版权 转载请注明原作者 并给出链接
write by 九天雁翎(JTianLing) -- www.jtianling.com
阅读全文....
起始目的很简单,整合Bullet及Ogre,找个能够生成.scene和.bullet文件的建模工具。
折腾一晚上Bullet及Ogre相关的东西,基本上就像爱迪生发明灯泡一样,得出了N个失败的教训,总结如下,大家不要再走弯路了。
1. Blender, 开源产品,我寄予了厚望,结果却是大大的失望,Blender的Ogre插件那个弱。。。。。Mesh导出还算可用,但是不能一次导出多个,要导出多个你就去吐血吧。而Scene导出插件简直就是跟你开国际玩笑,只能导出scene文件,需要你自己用Mesh插件导出所有的模型,于是乎,两者合作的结果是,你需要一个一个的导出mesh,然后再用Scene导出插件导出Scene。(不过经测试,的确可用)这个就算了,Blender内置bullet物理支持,编辑非常方便,在自己的Game Engine中模拟也是几乎完美,还支持constraints,我都惊叹了。。。。结果是,无法导出.bullet文件,用了erwin提供的改版blender,发现没有菜单,于是乎,google出了下列脚本:
import PhysicsConstraints
PhysicsConstraints.exportBullet("testFile.bullet")
运行之,发现没有PhysicsConstraints这个模块,再Google之,要先运行Game Engine(相当于动态加载了此模块),然后再运行脚本。(先按‘p'运行,然后按space键运行脚本)
好不容易出来个bullet文件,尝试加载之,一堆错误输出,有些物体被创建,但是可能因为坐标轴的问题导致重力方向不对,有点乱。。。。。。。。。。。
暂时放弃寄希望于Blender的mesh,scene导出(这个过程是在复杂)+.bullet文件导出了,那么用Blender自己的打包文件.blender吧,开始尝试GameKit。
2. GameKit,虽然以前试过,知道其还不成熟,到了现在感觉要用blender,那么就必须要用gamekit了(如上原因),这次硬着头皮实验,获取svn代码,直接用cmake生成iPhone工程(按官方文档方法所述),无法用Xcode打开,使用Gamekit工程中提供的改版cmake源码编译一套cmake,再生成工程,还是无法打开,下载官方发布的源代码,再生成,可以打开了(明显svn上最新的源码的cmake脚本有bug了)。编译,错误无数,查看到某人的类似经历,他竟然编译成功并且运行了,但是运行后错误同样惨不忍睹。遂放弃。。。。。
3. MAYA, OgreMax插件的MAYA版本同样异常出色,而Maya的bullet插件原来是迪斯尼做的,也还算好,支持简单的constraints。尝试载入了一个复杂的场景,导出bullet,结果载入时崩溃。自己尝试制作一个简单的场景,通过bullet的demo载入正常。也就是说估计哪里还是有问题,但是起码还算可用吧。然后尝试将此用Bullet插件建立的Max文件导出为scene,用Ogre载入,没有任何东西显示。。。。。这就悲剧了,同样是在MAYA当中,你要么使用Bullet的插件编辑Bullet的东西,可以到处.Bullet文件,但是无法显示,反之,用MAYA自身编辑的模型又无法拥有物理,无法导出.bullet文件,这是最大的悲剧,拆开可用,合起来不能用。。。。。、
4. 3Ds Max,
我最熟悉的工具,用于建模很好很强大,OgreMax这个Ogre的Scene导出也非常不错(除了愚蠢的不知道复制出来的模型可以用同一个mesh),
可以一次导出一个场景的所有模型的mesh文件及scene文件。最开始用Bullet的Max插件导出了一个.bullet文件无法使用,非常无奈。以为这个插件根本无法使用,差点放弃。但是因为实在没有太好的其他解决方案,所以后来自己通过插件源代码编译插件,希望新版插件能够修复一些bug,最后发现,问题的核心在于我通过plane创建出来的static的物体无法使用,(应该是转成bullet中的triangle mesh了,载入时会失败,但是其他的sphere和box等其实可以正常使用,包括普通的static物体。一下子让我在黑暗中看到了光明。。。。。。。。神啊。。。。。。。。。。。。。
但是其实还有几个问题,其一,物理和显示的关联。其二,bullet的坐标轴和Max的坐标轴不一样,导出的物理会有点混乱。但是,已经有路可以走了。。。。。。。。。。。
阅读全文....
write by 九天雁翎(JTianLing) -- www.jtianling.com
讨论新闻组及文件
前言
Bullet据称为游戏世界占有率为第三的物理引擎,也是前几大引擎目前唯一能够找到的支持iPhone,开源,免费(Zlib协议,非常自由,且商业免费)的物理引擎,但是文档资料并不是很好,Demo虽然多,但是主要出于特性测试/展示的目的,会让初学者无从看起,一头雾水。我刚学习Bullet的时候困于没有好的文档及资料,非常没有头绪,折腾了很久,所以就发挥没有就创造的精神,写作及整理此文,(以整理资料为主,自己写 为辅)希望大家在学习Bullet的时候不要再像我开始一样没有头绪。因为我实在没有精力去完成一个包含Bullet方方面面的完全指南,所以本文只能是不完全版本,这个就请大家谅解了,但是期望能够真正的完成一个简单的由浅入深的教程,并提供尽量详尽的额外信息链接,只能说让初学者比看官方的WIKI和Demo效果更好,大家有好的信息和资料而本文没有包含的,也请告诉我,我可以在新版中添加进来。因为我学习Bullet的时间也比较短,有不对的地方请高人指点。
前段时间简单的学习了一下Bullet,牵涉到图形部分的时候主要都是研究Bullet与Ogre的结合,所以使用了OgreBullet这个Ogre的Addon,其实真正的学习当然还是直接利用Bullet本身附带的简单的debug OpenGL绘制就好了。本文就完全以Bullet本身的Debug功能来学习,虽然简陋,但是可以排除干扰,专注于bullet。也许除了本文,会有个额外的文章,稍微研究下Ogre与Bullet的整合和分析一下OgreBullet的源码。
Bullet介绍
Bullet的主页
。最新版本在这里下载
。简单的中文介绍见百度百科
。一些也许可以促使你选择Bullet的小故事在以前的文章中有提及,参考这里
的开头--为什么选择Bullet。很遗憾的是前几天看到的一篇很详细的bullet中文介绍找不到了,将来也许补上。
安装
Bullet作为一款开源物理引擎,你可以选择作者编译好的SDK
,或者直接从源码编译自己的版本(Windows版本自带VS工程)。得益于CMake,在其他平台从源码自己编译也非常简单,参考这里
。iPhone版本的话参考这里
。想要更详细点的图文教程可以参考Creating_a_project_from_scratch
。
Hello World Application
在学习之前,没有接触过物理引擎的可以参考一下这个术语表
。
这里
有个较为详细的教程。也包含在Bullet本身的一个名叫 AppHelloWorld 的Demo中。(注释也很详细,但是和WIKI上的版本略有不同)可以大概的对Bullet有个感觉。
其实Bullet与Ogre走的一条路线,为了灵活,增加了很多使用的复杂性。(真怀念Box2D和Irrlicht的简单啊)其实即使希望通过strategy模式来增加灵活度,让用户可以自由的选择各类算法和解决方案,但是我还是感觉首先提供默认解决方案,用户需要不同方案的时候通过Set方式改变(甚至也可以new的时候修改)但是大牛们研究这些东西那么透,总是会觉得这个世界上不存在默认方案。。。。。因为没有方案是最优的,是适合大多数情况的,所以导致Bullet的HelloWorld程序源代码都已经超过100行。。。。。。。。。。-_-!发了点牢骚。。。。。
通过HelloWorld程序,我们大概可以知道一些东西,比如建立一个Bullet物理世界的步骤,比如Bullet的类以bt(变态-_-!)开头,比如Bullet与Box2D这样的2D物理引擎一样,专注于数据的计算,本身没有图形输出,比如创建一个物理实体的时候也有shape的概念,然后通过一个结构作为参数(BodyConstructionInfo)来创建真实的物体,大概的熟悉一下就好,具体的细节还不懂,没有关系,一步一步来。
另外,建议趁这个机会,确定自己机器使用Bullet的环境,特别是Win32下,我的使用方法是,利用BULLET_HOME环境变量指明Bullet安装的位置,BULLTE_LIBS指明最后编译完的静态库的位置,工程中利用这两个环境变量来确定位置。(这种用法很适合屏蔽各机器的环境不同)最后的Hello World工程见https://bullet-sample.jtianling.googlecode.com/hg/中的Bullet-HelloWorld。
请确保该Hello World程序能够运行(无论是你自己的还是用我的)然后才继续下面的内容。
让你坐在司机的位置上
该怎么学习的问题,向来都是各执一词,有人认为该从最基础的学起,就像建房子一样打好地基,有人会更加推崇自上而下的学习(Top-Down Approach),我属于后一派,能先写有用的可以摸到的程序,然后一层一层的向下学习,这样会更加有趣味性,并且学习曲线也会更加平缓,假如你是前一派,那么推荐你先看完Bullet的User Manual,然后是Bullet所有的Tutorial Articles
,然后再自己一个一个看Demo。
在Hello World的例子中你已经可以看到文本数据的输出,能够看到球/Box的落下了,但是很明显太不直观了,得益于Bullet良好的debug输出支持,我们要先能直观的通过图形看到球的落下!先坐在司机的位置上才能学会开车^^你也不至于被乏味的汽车/交通理论闷死。
Bullet像Ogre一样,提供了一个DemoApplication类,方便我们学习,我们先看看Bullet的DemoApplication是怎么样的。先看看Bullet自己提供的AppBasicDemo这个Demo。忽略那些作者用#if 0关闭的内容和hashmap的测试内容,看看DemoApplication的用法。
首先是BasicDemo类,从class BasicDemo : public PlatformDemoApplication可以看到,DemoApplication是给你继承使用的,这里的PlatformDemoApplication实际是GlutDemoApplication。(Win32那个作者好像只是预留的)
怎么去实现这个类先放一边,看看整个类的使用:
GLDebugDrawer gDebugDrawer;
BasicDemo ccdDemo;
ccdDemo.initPhysics();
ccdDemo.getDynamicsWorld()->setDebugDrawer(&gDebugDrawer);
glutmain(argc, argv,640,480,"Bullet Physics Demo. http://bulletphysics.com",&ccdDemo);
实际就这5句,很简单,构造debug,BasicDemo,调用initPhysics函数,设定debug,调用glutmain这个函数,参数也一目了然。这里就不看了。看实现一个有用的DemoApplication的过程。
大概看看DemoApplication这个基类和GlutDemoApplication知道必须要实现的两个纯虚函数是
virtual void initPhysics() = 0;
virtual void clientMoveAndDisplay() = 0;
看BasicDemo的实现后,知道还需要实现displayCallback这个现实回调,基本上就没有其他东西了,理解起来也还算容易。
initPhysics的部分,一看就知道,与HelloWorld中过程几乎一致,也就是实际构建物理世界的过程。只是多了
setTexturing(true);
setShadows(true);
setCameraDistance(btScalar(SCALING*50.));
这三个与显示有关的东西(其实这些代码放到myinit中去也可以,毕竟与物理无关)
最后还多了个clientResetScene的调用,我们知道这个过程就好,具体函数的实现先不管。
clientMoveAndDisplay和displayCallback部分
其实非常简单,几乎可以直接放到glutExampleApplication中去。(事实上不从灵活性考虑,我觉得放到glutExampleApplication中更好)
原来的程序有些代码重复,其实只要下列代码就够了:(一般的程序也不需要修改)
void
BasicDemo::clientMoveAndDisplay()
{
//simple dynamics world doesn't handle fixed-time-stepping
float
ms = getDeltaTimeMicroseconds();
///step the simulation
if
(m_dynamicsWorld)
{
m_dynamicsWorld->stepSimulation(ms / 1000000.f
);
}
displayCallback();
}
void
BasicDemo::displayCallback(void
) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderme();
//optional but useful: debug drawing to detect problems
if
(m_dynamicsWorld)
m_dynamicsWorld->debugDrawWorld();
glFlush();
swapBuffers();
}
运行该程序能够看到中间一个很多Box堆起来的大方块,点击鼠标右键还能发射一个方块出去。
了解这个Demo以后,我们就可以直接来用Bullet构建我们自己的物理世界了,暂时不用考虑图形的问题,甚至不用知道Bullet使用GLUT
作为debug图形的输出,GLUI
做界面,都不用知道,只需要知道上面demoApplication的使用和在initPhysics函数中完成构建物理世界的代码。另外,你愿意的话,也可以先看看exitPhysics的内容,用于分配资源的释放,作为C++程序,一开始就关注资源的释放问题是个好习惯。虽然对于我们这样简单的demo程序来说是无所谓的。
看过上面Demo后,也许你已经有些了解,也许你还是一头雾水,不管怎么样,Bullet的Demo毕竟还是别人的东西,现在,从零开始,构建一个HelloWorld程序描述的世界。先自己尝试一下!假如你成功了,那么直接跳过一下的内容,失败了,再回头了看看,提醒你步骤:
1.继承DemoApplication,拷贝上面clientMoveAndDisplay和displayCallback部分的代码,实现这两个函数。
2.在initPhysics函数中完成构建物理世界的代码。(构建过程参考HelloWorld)
3.Main中的使用代码:
GLDebugDrawer gDebugDrawer;
BasicDemo ccdDemo;
ccdDemo.initPhysics();
ccdDemo.getDynamicsWorld()->setDebugDrawer(&gDebugDrawer);
glutmain(argc, argv,640,480,"Bullet Physics Demo. http://bulletphysics.com",&ccdDemo);
4.注意工程需要多包含$(BULLET_HOME)/Demos/OpenGL的头文件目录
和库:
$(BULLET_HOME)/Glut/glut32.lib
opengl32.lib
glu32.lib
麻烦点的是glut是个动态库,你需要将dll文件拷贝到你工程的运行目录。
现在应该成功了吧?
我实现的工程见https://bullet-sample.jtianling.googlecode.com/hg/中的Bullet-WithGL。
于是乎,现在你已经可以看到Hello World中那个不曾显示的电脑凭空现象的球了。大概是下面这个样子滴:(因为现在Google Docs写完文章后很久就不能够直接post到CSDN的博客中去了,所以每次写完文章后都得到新建文章中去复制粘贴,图片还需要重新上传然后插入,非常麻烦,所以最近的文章都尽量的减少了图片的使用,见谅。其实说来,只有看热闹的人才需要截图,真的看教程的人估计自己的程序都已经运行起来了,也没有必要看我的截图了)
到目前为止,你已经有了可以自己happy一下的程序了,你可以立即深入的学习,去研究ExampleApplication的源代码,去了解Bullet是怎么与图形交互的,但是在这个之前,特别是对于以前没有使用过其他物理引擎的人,先多在这个图形版的HelloWorld的程序的基础上玩玩,比如现在球很明显没有弹性,调整一下反弹的系数看看,比如多生成几个球,比如加上速度,演示两个球的碰撞,比如因为现在没有设置debugmode,所以实际没有debug信息输出,尝试输出aabb等debug信息,有助于进一步学习。有了图形,就有了丰富的世界,先对Bullet的各个概念先熟悉一下(特别是btRigidBodyConstructionInfo中的各个变量,还有各个shape)然后再前进吧。事实上,得益于ExampleApplication,现在甚至可以用鼠标左键拖拽物理,右键发射箱子的功能也还在,还能按左右键调整camera。(其实还有一堆功能,自己尝试一下吧)因为比较简单,在本教程中不会再有关于这些基础信息的内容,只能自己去找资料或者尝试了。其实就我使用物理引擎的经验,学习并使用一款物理引擎并不会太难,最麻烦的地方在于后期游戏中各个参数的调整,尽量使游戏的效果变得真实,自然,或者达到你想要的效果,这种调整很多时候需要靠你自己的感觉,而这个感觉的建立,那就是多多尝试一个物理引擎中的在各个参数下呈现的不同效果了,这种感觉的建立,经验的获得,不是任何教程,文档或者演示程序能够给你的。
比如,下面是Bullet支持的5种基本物体形状:
其实上面的内容是最关键的,此时学开车的你已经在司机的位置了,学游泳的你已经在水里了,剩下的Bullet相关的内容虽然还有很多,但是其实已经完全可以自己独立折腾了,因为每个折腾的成果你都已经能够实时的看到,能够很哈皮的去折腾了。
与显示的整合,MotionState
一个只有数据运算的物理引擎,一般而言只能为显示引擎提供数据,这就牵涉到与图形引擎整合的问题,像Box2D这样的物理引擎就是直接需要直接向各个物理实体去查询位置,然后更新显示,这种方式虽然简单,但是我感觉非常不好,因为难免在update中去更新这种东西,导致游戏逻辑部分需要处理物理引擎+图形引擎两部分的内容。(可以参考Box2D与Cocos2D for iPhone的整合)而且,对于完全没有移动的物体也会进行一次查询和移动操作。(即使优化,对不移动物体也是进行了两次查询)
Bullet为了解决此问题,提供了新的解决方案,MotionState。其实就是当活动物体状态改变时提供一种回调,而且就Bullet的文档中说明,此种回调还带有适当的插值以优化显示。通过这种方法,在MotionState部分就已经可以完成显示的更新,不用再需要在update中添加这种更新的代码。而且,注意,仅仅对活动物体状态改变时才会进行回调,这样完全避免了不活动物体的性能损失。
首先看看ExampleApplication中是怎么利用default的MotionState来显示上面的图形的,然后再看看复杂点的例子,与Ogre的整合。
先看看回调接口:
///The btMotionState interface class allows the dynamics world to synchronize and interpolate the updated world transforms with graphics
///For optimizations, potentially only moving objects get synchronized (using setWorldPosition/setWorldOrientation)
class
btMotionState
{
public
:
virtual
~btMotionState()
{
}
virtual
void
getWorldTransform(btTransform& worldTrans ) const
=0
;
//Bullet only calls the update of worldtransform for active objects
virtual
void
setWorldTransform(const
btTransform& worldTrans)=0
;
};
很简单,一个get接口,用于bullet获取物体的初始状态,一个set接口,用于活动物体位置改变时调用以设置新的状态。
下面看看btDefaultMotionState这个bullet中带的默认的MotionState类。
///The btDefaultMotionState provides a common implementation to synchronize world transforms with offsets.
struct
btDefaultMotionState : public
btMotionState
{
btTransform m_graphicsWorldTrans;
btTransform m_centerOfMassOffset;
btTransform m_startWorldTrans;
void
* m_userPointer;
btDefaultMotionState(const
btTransform& startTrans = btTransform::getIdentity(),const
btTransform& centerOfMassOffset = btTransform::getIdentity())
: m_graphicsWorldTrans(startTrans),
m_centerOfMassOffset(centerOfMassOffset),
m_startWorldTrans(startTrans),
m_userPointer(0
)
{
}
///synchronizes world transform from user to physics
virtual
void
getWorldTransform(btTransform& centerOfMassWorldTrans ) const
{
centerOfMassWorldTrans = m_centerOfMassOffset.inverse() * m_graphicsWorldTrans ;
}
///synchronizes world transform from physics to user
///Bullet only calls the update of worldtransform for active objects
virtual
void
setWorldTransform(const
btTransform& centerOfMassWorldTrans)
{
m_graphicsWorldTrans = centerOfMassWorldTrans * m_centerOfMassOffset ;
}
};
这个默认的MotionState实现了这两个接口,但是还引入了质心
(center Of Mass应该是指质心
吧)的概念,与外部交互时,以质心位置表示实际物体所在位置。
在一般rigitBody的构造函数中可以看到下列代码:
if
(m_optionalMotionState)
{
m_optionalMotionState->getWorldTransform(m_worldTransform);
} else
{
m_worldTransform = constructionInfo.m_startWorldTransform;
}
这就是get函数的使用,也就是决定物体初始坐标的函数回调。
set函数的回调如下:
void
btDiscreteDynamicsWorld::synchronizeSingleMotionState(btRigidBody* body)
{
btAssert(body);
if
(body->getMotionState() && !body->isStaticOrKinematicObject())
{
//we need to call the update at least once, even for sleeping objects
//otherwise the 'graphics' transform never updates properly
///@todo: add 'dirty' flag
//if (body->getActivationState() != ISLAND_SLEEPING)
{
btTransform interpolatedTransform;
btTransformUtil::integrateTransform(body->getInterpolationWorldTransform(),
body->getInterpolationLinearVelocity(),body->getInterpolationAngularVelocity(),m_localTime*body->getHitFraction(),interpolatedTransform);
body->getMotionState()->setWorldTransform(interpolatedTransform);
}
}
}
也就是同步状态的时候调用。此过程发生在调用bullet的btDynamicsWorld::stepSimulation函数调用时。
然后可以参考DemoApplication的DemoApplication::renderscene(int pass)函数:
btScalar m[16
];
btMatrix3x3 rot;rot.setIdentity();
const
int
numObjects=m_dynamicsWorld->getNumCollisionObjects();
btVector3 wireColor(1
,0
,0
);
for
(int
i=0
;i<numObjects;i++)
{
btCollisionObject* colObj=m_dynamicsWorld->getCollisionObjectArray()[i];
btRigidBody* body=btRigidBody::upcast(colObj);
if
(body&&body->getMotionState())
{
btDefaultMotionState* myMotionState = (btDefaultMotionState*)body->getMotionState();
myMotionState->m_graphicsWorldTrans.getOpenGLMatrix(m);
rot=myMotionState->m_graphicsWorldTrans.getBasis();
}
}
}
实际也就是再通过获取motionState然后获取到图形的位置了,这种defaultMotion的使用就类似Box2D中的使用了。
既然是回调,那么就可以让函数不仅仅做赋值那么简单的事情,回头来再做一次轮询全部物体的查询,官网的WIKI中为Ogre编写的MotionState就比较合乎推荐的MotionState用法,代码如下:
lass MyMotionState : public
btMotionState {
public
:
MyMotionState(const
btTransform &initialpos, Ogre::SceneNode *node) {
mVisibleobj = node;
mPos1 = initialpos;
}
virtual
~MyMotionState() {
}
void
setNode(Ogre::SceneNode *node) {
mVisibleobj = node;
}
virtual
void
getWorldTransform(btTransform &worldTrans) const
{
worldTrans = mPos1;
}
virtual
void
setWorldTransform(const
btTransform &worldTrans) {
if
(NULL
== mVisibleobj) return
; // silently return before we set a node
btQuaternion rot = worldTrans.getRotation();
mVisibleobj->setOrientation(rot.w(), rot.x(), rot.y(), rot.z());
btVector3 pos = worldTrans.getOrigin();
mVisibleobj->setPosition(pos.x(), pos.y(), pos.z());
}
protected
:
Ogre::SceneNode *mVisibleobj;
btTransform mPos1;
};
注意,这里的使用直接在set回调中直接设置了物体的位置。如此使用MotionState后,update只需要关心逻辑即可,不用再去手动查询物体的位置,然后更新物体的位置并刷新显示。
碰撞检测
物理引擎不仅仅包括模拟真实物理实现的一些运动,碰撞,应该还提供方式供检测碰撞情况,bullet也不例外。
AppCollisionInterfaceDemo展示了怎么直接通过btCollisionWorld来检测碰撞而不模拟物理。
而官方的WIKI对于碰撞检测的描述也过于简单,只给下列的示例代码,但是却没有详细的解释。
//Assume world->stepSimulation or world->performDiscreteCollisionDetection has been called
int
numManifolds = world->getDispatcher()->getNumManifolds();
for
(int
i=0
;i<numManifolds;i++)
{
btPersistentManifold* contactManifold = world->getDispatcher()->getManifoldByIndexInternal(i);
btCollisionObject* obA = static_cast
<btCollisionObject*>(contactManifold->getBody0());
btCollisionObject* obB = static_cast
<btCollisionObject*>(contactManifold->getBody1());
int
numContacts = contactManifold->getNumContacts();
for
(int
j=0
;j<numContacts;j++)
{
btManifoldPoint& pt = contactManifold->getContactPoint(j);
if
(pt.getDistance()<0.f
)
{
const
btVector3& ptA = pt.getPositionWorldOnA();
const
btVector3& ptB = pt.getPositionWorldOnB();
const
btVector3& normalOnB = pt.m_normalWorldOnB;
}
}
}
以上代码的主要内容就是
int
numManifolds = world->getDispatcher()->getNumManifolds();
btPersistentManifold* contactManifold = world->getDispatcher()->getManifoldByIndexInternal(i);
两句。
而btPersistentManifold类表示一个Manifold,其中包含了body0,body1表示Manifold的两个物体。
这里特别提及的是,Manifold并不直接表示碰撞,其真实的含义大概是重叠,在不同的情况下可能表示不同的含义,比如在Box2D中,手册的描述大概是(凭记忆)为了快速的检测碰撞,在2D中一般先经过AABB盒的检测过滤,而只有AABB盒重叠的才有可能碰撞,而Manifold在Box2D中就表示AABB盒重叠的两个物体,而我看Bullet有不同的Broadphase,在实际中,也重叠也应该会有不同的情况,因为我没有看源码,所以不能确定,但是,总而言之,可以理解Manifold为接近碰撞的情况。
所以无论在Box2D还是Bullet中,都有额外的表示碰撞的概念,那就是contact(接触)。上述示例代码:
int
numContacts = contactManifold->getNumContacts();
就表示查看接触点的数量,假如接触点为0,那么自然表示两个物体接近于碰撞,而实际没有碰撞。而上述代码中的Distance的判断应该是防止误差,因为我输出了一个盒子和地面发生碰撞的全部过程的distance,发现绝大部分情况,只要有contact,那么距离就小于0,可是在一次盒子离开地面的过程中,distance还真有过一次0.00x的正值。。。。。。。
当你开始放心大胆的使用上述代码后,也许你总是用来模拟物体的其他效果,也许都不会有问题,直到某一天你希望在碰撞检测后删除掉发生碰撞的问题,你的程序crash了。。。。你却不知道为什么。用前面的demo来展示碰撞检测的方法,并且删除掉发生碰撞的物体。一般先写出的代码都会类似下面这样:
int
numManifolds = m_dynamicsWorld->getDispatcher()->getNumManifolds();
for
(int
i=0
;i<numManifolds;i++)
{
btPersistentManifold* contactManifold = m_dynamicsWorld->getDispatcher()->getManifoldByIndexInternal(i);
btCollisionObject* obA = static_cast
<btCollisionObject*>(contactManifold->getBody0());
btCollisionObject* obB = static_cast
<btCollisionObject*>(contactManifold->getBody1());
int
numContacts = contactManifold->getNumContacts();
for
(int
j=0
;j<numContacts;j++)
{
btManifoldPoint& pt = contactManifold->getContactPoint(j);
if
(pt.getDistance()<0.f
)
{
RemoveObject(obA);
RemoveObject(obB);
}
}
}
但是上面这样的代码是有问题的,这在Box2D的文档中有详细描述,Bullet文档中没有描述,那就是obA和obB可能重复删除的问题(也就相当于删除同一个对象多次,自然crash)在本例中有两个问题会导致重复,很明显的一个,当两个物体多余一个Contact点的时候,在遍历Contacts点时会导致obA,obB重复删除。另外,稍微隐晦点的情况是,当一个物体与两个物体发生碰撞时,同一个物体也可能在不同的manifold中,所以,真正没有问题的代码是先记录所有的碰撞,然后消除重复,再然后删除
。这是Bullet文档中没有提到,WIKI中也没有说明的,初学者需要特别注意。。。。。。下面才是安全的代码:
int
numManifolds = m_dynamicsWorld->getDispatcher()->getNumManifolds();
for
(int
i=0
;i<numManifolds;i++)
{
btPersistentManifold* contactManifold = m_dynamicsWorld->getDispatcher()->getManifoldByIndexInternal(i);
btCollisionObject* obA = static_cast
<btCollisionObject*>(contactManifold->getBody0());
btCollisionObject* obB = static_cast
<btCollisionObject*>(contactManifold->getBody1());
int
numContacts = contactManifold->getNumContacts();
for
(int
j=0
;j<numContacts;j++)
{
btManifoldPoint& pt = contactManifold->getContactPoint(j);
if
(pt.getDistance()<0.f
)
{
m_collisionObjects.push_back(obA);
m_collisionObjects.push_back(obB);
}
}
}
m_collisionObjects.sort();
m_collisionObjects.unique();
for
(CollisionObjects_t::iterator itr = m_collisionObjects.begin();
itr != m_collisionObjects.end();
++itr) {
RemoveObject(*itr);
}
m_collisionObjects.clear();
上述m_collisionObjects是std::list类型的成员变量。
碰撞过滤
Bullet的wiki
提到了3个方法,这里只讲述最简单的mask(掩码)过滤方法。
mask的使用相信大家基本都接触过,无非就是通过一个整数各个2进制位来表示一些bool值。比如Unix/Linux中文件权限的掩码。在bullet中的碰撞mask的使用非常简单,主要在addRigidBody时候指定。(需要注意的是,只有btDiscreteDynamicsWorld类才有这个函数,btDynamicsWorld并没有,所以demoApplication中的成员变量dynamicWorld不能直接使用。)
WIKI中的代码已经很能说明问题了:
#define BIT(x) (
1
<<(x))
enum
collisiontypes {
COL_NOTHING = 0
, //<Collide with nothing
COL_SHIP = BIT(1
), //<Collide with ships
COL_WALL = BIT(2
), //<Collide with walls
COL_POWERUP = BIT(3
) //<Collide with powerups
}
int
shipCollidesWith = COL_WALL;
int
wallCollidesWith = COL_NOTHING;
int
powerupCollidesWith = COL_SHIP | COL_WALL;
btRigidBody ship; // Set up the other ship stuff
btRigidBody wall; // Set up the other wall stuff
btRigidBody powerup; // Set up the other powerup stuff
mWorld->addRigidBody(ship, COL_SHIP, shipCollidesWith);
mWorld->addRigidBody(wall, COL_WALL, wallCollidesWith);
mWorld->addRigidBody(powerup, COL_POWERUP, powerupCollidesWith);
特别是那个#define BIT(x) (
1
<<(x))
宏用的很有意思。
不要特别注意的是,两个物体要发生碰撞,那么,两个物体的collidesWith参数必须要互相指定对方,假如A指定碰撞B,但是B没有指定碰撞A,那么还是没有碰撞。就上面的例子而言,虽然ship和powerup想要撞墙,但是墙不想撞它们,那么事实上,上面的例子就相当于过滤了所有墙的碰撞,其实仅仅只有ship和power的碰撞,这真所谓强扭的瓜不甜啊,等双方都情愿。
仿照上面的例子,假如你希望在碰撞检测的时候过滤掉地板,只让物体间发生碰撞然后删除物体,为demo添加下列代码:
#define BIT(x) (
1
<<(x))
enum
collisiontypes {
COL_NOTHING = 0
, //<Collide with nothing
COL_GROUND = BIT(1
), //<Collide with ships
COL_OBJECTS = BIT(2
), //<Collide with walls
};
short
GroundCollidesWith = COL_OBJECTS;
short
ObjectsCollidesWith = COL_GROUND;
但是当你将上述方法应用到demo中,想要过滤掉你想要的碰撞,你会发现碰撞检测的确是过滤掉了,同时过滤掉的还有碰撞,球直接传地板而过,掉进了无底的深渊。注意,这里的过滤是指碰撞过滤,而不是碰撞检测的过滤,假如希望实现碰撞检测的过滤,你可以在碰撞检测中直接进行。比如前面地板的例子,因为地板是静态物体,你可以通过调用rigidBody的isStaticObject来判断是否是地板,然后进行删除,就如下面的代码这样:
if
(pt.getDistance()<0.f
) {
if
(!obA->isStaticObject()) {
m_collisionObjects.push_back(obA);
}
if
(!obB->isStaticObject()) {
m_collisionObjects.push_back(obB);
}
}
假如希望与地面碰撞并不删除物体,只有物体与物体的碰撞才删除物体,这也简单:
if
(!obA->isStaticObject() && !obB->isStaticObject()) {
m_collisionObjects.push_back(obA);
m_collisionObjects.push_back(obB);
至于更加复杂的情况,还可以借助于rigidBody的UserPointer,这在WIKI中没有提及,
///users can point to their objects, userPointer is not used by Bullet
void
* getUserPointer() const
{
return
m_userObjectPointer;
}
///users can point to their objects, userPointer is not used by Bullet
void
setUserPointer(void
* userPointer)
{
m_userObjectPointer = userPointer;
}
但是就我的经验,这两个函数的作用是巨大的,你可以将你需要的一切都设置进去。。。。。。。。然后取出来,就上面的碰撞检测过滤而言,你完全可以实现自己的一套碰撞检测mask,只要你想,一切皆有可能。这些例子的完整源代码见https://bullet-sample.jtianling.googlecode.com/hg/中的Bullet-CollideDetection工程。
约束(Constraints)和连接(Joints)
一个一个单独的物理实体已经可以构建一个有意思的物理世界了,但是显示世界有很多东西(最典型的就是绳子连接的物体)不是单独的物理实体可以模拟的,物理引擎中使用约束来模拟类似的东西/现象。
(待补充)
软体
因为我的使用暂时不需要用到软体,暂时未学习此部分内容,欢迎大家补充。
有用的工具
1. MAYA,3D Max的插件
2. Blender
,开源的3D建模工具,内建的Game Engine有直接的Bullet支持,还有Erwin提供的改版
可以直接导出.bullet文件。
使用了Bullet的其他有用工程
1. GameKit
,Erwin Coumans自己发起的整合Ogre/Irrlicht和Bullet的游戏引擎,与Blender结合的也很好。
2. oolongengine
,乌龙引擎,wolfgang.engel
(他的博客
)这个大牛(到底有多牛可以参考这里
)发起的iPhone平台引擎项目,使用了Bullet,也为Bullet能够流畅的运行于iPhone平台做出了很大贡献。(优化了浮点运算)为什么写乌龙引擎?wolfgang自己有个解释,见这里
。
3. Dynamica
, Erwin建立的工程,开发bullet的Maya,3D Max插件。
4. bullet-physics-editor
, Erwin自己发起的一个bullet编辑器工程,目前还处于前期开发阶段。但是项目中同时包含一些能够到处.Bullet文件的Blender改版
。
Bullet的实现原理
1.Physics Pipeline Implementation
,应该是一个在爱尔兰的中国人写的,并发布在WIKI上,这里是他的博客
。
2.SIGGRAPH 2010 course slides, high-level overview on Bullet collision detection
3.GDC 2010 presentation about contact generation
4.最大的资料自然就是Bullet的源代码
啦。。。。。。慢慢研究吧。
参考资料
1.Bullet 2.76 Physics SDK Manual
,Bullet的项目发起人,目前的负责人Erwin Coumans所写,(就WIKI资料显示,这哥们现在在SONY)算是官方的Manual了,源码包中就有pdf。
2.Bullet WIKI Tutorial Articles
,算是第2个能够找到的稍微好点的关于Bullet的东西了,就是有点散乱。
3.Bullet Bullet Documentation
,Bullet的文档,自动生成的,也就只能在写代码的时候可能有些用,很难靠这个学习。
我写的关于Bullet的文章
1.Ogre与Bullet的整合(on iPhone)
2.Ogre的3D Max插件介绍
最后,因为本文较长,为方便查看,提供了pdf版
方便大家查看。
原创文章作者保留版权 转载请注明原作者 并给出链接
write by 九天雁翎(JTianLing) -- www.jtianling.com
阅读全文....
write by 九天雁翎(JTianLing) -- www.jtianling.com
讨论新闻组及文件
Sierpinski triangle
的图形我早就见过了,是很漂亮的分形图形,只是今天才知道它叫这么个复杂的名字,很显然是以某个人命名的,因为我不研究分形,所以也不管他了。
只是最近看《Interactive Computer Graphics》(5nd Edward
Angel著)一书的时候,作者在讲解OpenGL
API的时候就引入了这个有意思的图形,本书作为讲图形学的技术书籍(还是作为美国标准教材类型写作的),算是别具趣味性了。里面有几个绘制
Sierpinski triangle的例子(书中称此图形为Sierpinski
gasket,是一样的),算是符合我以前学习的精神,用学到的最少的几个知识点,鼓捣出最大的乐趣。^^原文中的例子都是仅带源代码文件,(不带工程)
源代码还是仅仅只能一个一个下载,而且显示的时候是一次性显示的,这个我感觉不太爽,不能显示出这种分形图形生成时那种渐变的感觉,特别是用点随机生成的
那种类似粒子系统的乱中有序的效果,于是乎,一方面,给我感兴趣的原始版本配上XCode工程,(需要VS工程的就只能自己鼓捣了)另外一方面,提供动态
生成图形的例子,也算是加深理解。。。。。。
阅读全文....
write by 九天雁翎(JTianLing) -- www.jtianling.com
讨论新闻组及文件
其实早就看到了(很老了,写于Sunday, December 9, 2007),最近写Bullet相关文章的时候又翻看到了,想到常常有人会问到底哪个开源引擎比较好,这里就顺便简单的翻译一下吧。虽然是一家之言,而且Wolfgang Engel这个大牛(不了解有多牛可以参考这里
)的话是针对移动平台的,但是还是有一些普遍的参考价值的。来源
。这也是Wolfgang写乌龙引擎(oolongengine
)的初衷。
译文:
(括号里面的话都是我加的,单纯的翻译好无聊啊。。。。。。。)
对几大开源引擎的点评:
Ogre:架构和设计不太关注效率。其C++的用法导致使用和重新设计都比较困难。一个例子:每个材质都有它自己的一个C++文件并且这里还有一个复杂的继承链.... (现在Ogre已经官方支持iPhone了,也不需要Wolfgang来移植了,但是效率的确是个问题)
Irrlicht:我尝试的Mac OS X版本看起来像Quake 3引擎。它看起来缺少很多现代3D引擎设计的元素,除了看起来非常适合移动设备。因此,你可能也就想使用原来的Quake3引擎..... (的确很适合移动设备)
Quake 3:这是个很显然有很多优秀工具,非常有效率的引擎,我以前开发荣誉勋章系列的时候就使用了这个引擎,但是我想要更加灵活一些,并且我想以更加高级的硬件为目标。 (Quake 3实在也太老了点,现在也支持iPhone了)
Crystal Space:为什么每个东西都是一个插件?想不通。
C4:这是我最习惯的引擎之一,但是它是闭源的:-( (对开源引擎的评价怎么会加上这个闭源引擎?-_-!)
所以,我现在想要写我自己的,基于底层框架的引擎。(也就是乌龙引擎)
原文:
Evaluated several open source engines:
- Ogre:
the architecture and design is not very performance friendly. The usage
of C++ makes the usage and re-design here quite difficult. An example:
each material has its own C++ file and there is an inheritance chain
from a base class ...
- Irrlicht: the Mac OS X version I tried
looks like a Quake 3 engine. It also seems to lack lots of design
elements of a modern 3D engine. Other than this it looks quite good for a
portable device. You might also use the original Quake 3 engine then
...
- Quake 3: this is obviously a very efficient game engine with
rock-solid tools, I worked with this engine in the Medal of Honor
series before, but I wanted a bit more flexibility and I wanted to
target more advanced hardware.
- Crystal Space: why is everything a plug-in? Can't get my head around this.
- C4: this is one of my favourite engines, but it is closed source :-(
So I want to write my own based on the low-level framework I have in place now.
原创文章作者保留版权 转载请注明原作者 并给出链接
write by 九天雁翎(JTianLing) -- www.jtianling.com
阅读全文....