九天雁翎的博客
如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。 -- Paul Graham

从源码编译新版OGRE 1.7.2 [Cthugha] for iphone/ipad

新版本的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个已知点了,那么直线的点斜式方程就非常容易求出来了。

国外的教科书一般如下:

直线点斜式方程: y = m/times x + b

(式1)

斜率 m = /frac{(y_{End} - y_0)}{ (x_{End} - x_0)}

截距 b = y0 - m/times x0

将m,b带入式1,对于直线上任意一点,知道x坐标,那么

 y = (x - x_0) /times m + y_0

(式2)

当然,假如更加直观的用直线两点式方程来算y,会更加直接。

通过

直线两点式方程:/frac{(x - x_0)}{ (x_{End} - x_0)} = /frac{(y - y_0)}{(y_{End} - y_0)}

(式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,可以直接求得:
x = /frac{(y - y_0)}{m} + x_0

(式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个象限的绘制。

圆的参数方程:
x = rcos/theta

y = rsin/theta

  (式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)右边乘以/Delta{x}

,以形成Bresenham对决策参数的定义式。

3.你怎么样都不能在书中提及的条件下,将x0,y0,以及m=/frac{/Delta{y}}{/Delta{x}}

带入式(3.14)计算得到p_0 = 2/Delta{y}-/Delta{x}

,这里还需要用到原作者论文中提到过的假设,那就是x_0 = 0, y_0=0,b=0

,x_{end}=/Delta{x},y_{end}=/Delta{y}

,因为作者在之前有坐标系平移的一步,将新坐标系的原点移到(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相关工具的成果 -- 3Ds Max,Maya, blender, GameKit

起始目的很简单,整合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的坐标轴不一样,导出的物理会有点混乱。但是,已经有路可以走了。。。。。。。。。。。

阅读全文....

Bullet物理引擎不完全指南(Bullet Physics Engine not complete Guide)

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

 

阅读全文....

谢尔宾斯基三角(Sierpinski triangle)的显示 (with OpenGL)

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

 

阅读全文....

整合Bullet物理引擎到Ogre on iPhone

write by 九天雁翎(JTianLing) -- www.jtianling.com

讨论新闻组及文件

为什么选择Bullet

    Bullet
算是一个比较流行的3D物理引擎了,大概的看了几眼以后,了解了一些基本用法,发现很多3D物理中的概念与2D(比如Box2d)中的概念是相同的,甚至,Bullet的一些用法都与Box2D类似。基本了解以后,对于我来说,那就是iPhone平台的问题了。
    我选择Bullet而不是其它一大堆同样著名的物理引擎,主要源自乌龙(oolongengine
)
引擎,该引擎由wolfgang.engel最先创建,并且内嵌了bullet的支持,竟然如此大牛都青睐的引擎,我怎么能无视呢?何
况,oolongengine的使用,也说明Bullet在iPhone中使用完全没有问题。另外,还有Blender这个非常著名的3D建模工具也是对
Bullet有直接支持,可见Bullet的流行程度。事实上,还有些故事,比如oolongengine的项目负责人之一erwin.coumans,
同时就也是Bullet的项目创建者兼现在的负责人。(到Google
Code上去看看就知道了)并且,erwin提到,Bullet能够流畅的在iPhone上运行,wolfgang提供了很大的帮助,对浮点运算进行了特
别优化。最有意思的是,迪斯尼公司,自己的一些项目用到了Bullet,(看主页上的介绍,起码玩具总动员3这个游戏用到了Bullet)所以开发了自己
的MAYA
Bullet插件,为了回报开源社区,已经将此插件开源了。。。。。感谢Bullet,也感谢迪斯尼,同时感叹国外这种开源社区之间的交互。。。。。呵
呵,这才叫欣欣向荣的良性发展,你帮助我,我帮助你,公司受到帮助,也对社区进行回报。知道了这么多故事以后,更加是对Bullet多了很多好感。最值得
一提的是,erwin
简直就是个开源狂人。。。。。他还发起过一个叫做gamekit
的开源游戏引擎项目,希望整合Ogre/Irrlicht和Bullet,因为是erwin创建的项目,也非常值得期待。。。。。。。
    闲话多说一向是我的毛病。。。。也就说到这里了,用以前的OGRE on iPhone
工程直接开工了。

在XCode中编译Bullet和OgreBullet

   
初下载Bullet后,用CMake做工程,只能做Mac OS
X的工程,没有iPhone的选项,于是参考一下乌龙引擎的做法,就是将整个Src目录都拷贝进自己的工程,好像是从iPhone开始,流行这种"暴力"
使用源代码的方式了。。。。。只能说Apple的XCode开发的还不足够人性化,所以建库的工程没有VS那么方便,再加上iPhone天生的不支持动态
库,更加助长了这种“暴力”使用源代码的方式,其实每次修改工程文件编译都会慢很多,无奈啊。。。。。在Bullet的论坛中,搜索到erwin的准官方解决方案
就是拷贝全部目录。。。。汗一个-_-!
    既然如此,一切倒是简单了。。。。。下载Bullet的源码
,目前最新的是2.77,拷贝Src目录,删掉无用文件,比如CMake的一些文件。(或者直接从oolong引擎中将整个Bullet目录拷贝过来最简单,只不过版本目前是2.73)然后配置Bullet的Include目录,编译,一切OK。
   

现在开始尝试嵌入OgreBullet,方法还是直接包含源代码。比较特殊点的是OgreBullet需要用到Bullet的
ConvexDecomposition,这个库在Bullet的Extra中,也将源代码都拖过来,然后弄好include目录,就没有问题了。

测试

    现在进入测试阶段,就用OgreBullet的Tutorial

的例子。源代码全部拷贝过来,唯一的问题是ExampleApplication在iPhone中有些小问题,修改一下函数,namespace后问题解
决。运行时崩溃,查看问题,还是ExampleApplication这个类的问题,难怪在Ogre的iPhone
template不用这个类,崩溃的地方很有意思,OIS获取键盘的时候:
mKeyboard = static_cast<OIS::Keyboard*>(mInputManager->createInputObject( OIS::OISKeyboard, bufferedKeys ));
注释更加有意思:
//Create all devices (We only catch joystick exceptions here, as, most people have Key/Mouse)
典型反映了代码永远赶不上时代变化,既然这个类已经不被人使用了,我也就不费劲去用了,将原来的例子代码全部嵌入到OgreFramework类中。
运行,崩溃,发现忘了添加新增的资源,将BumpyMetal.jpg材质和cube.mesh模型添加进工程。再次运行,一些正常,有图有真相:

当一切都OK以后,我发现我的目标竟然与GameKit是一样的。。。。。不就是Ogre+Bullet吗?erwin
估计以前就已经想过我所想了。。。。。也许,尝试下GameKit也不错。

 

原创文章作者保留版权 转载请注明原作者 并给出链接

write by 九天雁翎(JTianLing) -- www.jtianling.com

阅读全文....

Bullet的3D Max插件

write by 九天雁翎(JTianLing) -- www.jtianling.com

讨论新闻组及文件

准备

    早就知道Bullet有个比较好的MAYA插件,那是迪斯尼自己的东西,然后开源的,在bullet的网站就有下载
。但是一直一来都是学习3D Max,接触了一下MAYA,感觉不是太习惯,有人说MAYA的操作方式接近设计师的思维。。。。我果然不是个设计师啊。。。。。我甚至都准备转投Blender了,好歹那也是开源免费的东西,而且与Bullet结合的还不错,还有GameKit这个好玩的东西在。我初步的看了一下,感觉GameKit有点类似LOVE这个Lua的2D游戏引擎,以一个数据包作为输入,然后是一个独立的可执行程序,只不过LOVE中的数据包是zip包,集合了一堆的Lua脚本和资源,而GameKit的数据包是由Blender生成的,也可以包含Lua脚本,正在想呢,这个东西是不是太前卫啊?在Blender中通过一堆的UI去弄逻辑?作为程序员,好像还是比较习惯编写代码。。。。。呵呵,为了Bullet,前段时间刚刚了解了一下Blender的2.4x版本,结果发现GameKit支持的blender的新版(有点想3D Max了)界面差异好大啊。。。也是个打击。。。。。就在此时,无意识的(我也不知道为什么)到Bullet的插件独立网站dynamica

中去逛了一下,才发现,原来Bullet已经有3D Max的插件了,改装自Nvidia开源的PhyX的3D Max插件,(感谢伟大的NVidia公司,虽然它为了自家的Cg对OpenGL的GLSL有些冷遇,导致我需要用AMD的显卡来运行RenderMonkey才方便学习,不过因此我都原谅他了-_-!)甚至还有最新的2011版本的。。。。。天哪,我都在费什么劲啊。。。。晃了那么大一圈,果然,人生在于折腾。。。。。暂时告别下MAYA和Blender了,继续弄弄3D Max先。

    安装完插件后,发现Bullet的3D Max插件文档也比MAYA的要丰富啊(MAYA的主要文档就是指向几个教学视频)。。。。。太感谢开源社区了。。。。。。安装后的3D Max效果如图:

实验

有些奇怪的是,安装完后虽然有Example目录,文档中也提到过Example,但是实际没有任何Example,文档这么详细,使用这么简单,随便学学就能懂了,自己弄个Example^^

先用2D Array复制出一些堆起来的盒子,然后弄个球撞之。。。。。。。。。

呵呵,和文档中的例子截图有些像了^^同时发现复制时是可以带物理属性复制的,但是物理的模拟与3D Max的关键帧动画结合的并不好,不能回滚,导致我想回来截个好图都没有办法-_-!(只能重新再从头模拟一次)

经过试用,发现使用上还是有些不方便,比如每次修改物理属性后都需要add Selected或者add All Phys,这个不太合理,然后就是,目前完全不支持Joint或者Contrainter,也就是说,几乎也就是只有刚体和软体的支持。还有,初始速度和旋转不知道为啥,在我这里也无效,文档中没有提及。

说到这里,无法设定初始速度,那上面的例子是怎么做出来的呢?^^现在揭秘答案:

哈哈...............................

小结

    总的来说,bullet的3D Max插件已经比较不错了^^别说别的,就算当一个简单,方便的Bullet文件编辑器用都好啊,比起每个物体通过代码去设置属性,Bullet文件要方便太多了。最重要的是,3D Max对我来说,还是最亲切的。。。。。。暂时聊胜于无了,不够用的时候,再去求助于MAYA插件或者Blender吧。

 

注意:事实证明,目前的此插件到处的bullet文件无法正常加载,再未更新之前,请直接忽略之。

 

原创文章作者保留版权 转载请注明原作者 并给出链接

write by 九天雁翎(JTianLing) -- www.jtianling.com

 

阅读全文....

论XMarks的死掉及利益是第一生产力。。。。。。。。

    今天看到电脑玩物

的一则消息:根据XMarks的博客

发布的消息,由于找不到合适的盈利途径,XMarks将于2011年1月10日

停止服务。。。。。看看到这则消息我非常感叹,因为XMarks本身实在是一个非常好用的产品,也算是展示中国互联网环境到底多么开放的一个镜子,(Dropbox也是)一直以来,也根本没有替代品,(Firefox sync可以部分替代,但是XMarks是跨浏览器的)对于一个这么好的产品的死掉,总是引人无限的感叹,不禁引人深思,为什么一个好产品却不能生存下去?

    那就是一个产品的关键了 ----盈利,很明显,一个产品只有盈利才能长久的走下去,不能盈利的产品,无论多么优秀,无论用户的评价多么高,无论多么实用,无论满足了多少用户的需求(就像XMarks),最终还是会死掉。这是很悲哀,也很无奈的现实。现代社会发展的最大动力那就是利益,这是资本主义能成为地球上最成功的经济模式的基础,也是人类本性决定的。

    我也许会对迅雷,qq,暴风影音无时不刻的广告,突然弹出来影响你工作的广告感到非常的愤怒,也许会受不了搜狗的输入法在升级的时候还隐含带着浏览器的安装的流氓行为,然后连着输入法一起卸载了,并且在卸载原因里面告诉搜狗的人,卸载原因是因为你们做的就是流氓软件,也许会看着flashgot频繁的一次又一次仅仅升级版本号的更新,就是为了让你进去他的主页看看广告而多次想删掉它,但是在XMarks死掉后,也许我能原谅他们了。。。。。比起不想看到广告,我更不想看到它们死掉。。。。。

    以前有领导人提出,科学技术是第一生产力,事实上,我想其实他也知道,利益才是第一生产力,所以,他也是这样改革中国经济的。有利益的地方才有生产力。由此能想到app store的繁荣,而android的设备数量(指用户的持有量)虽然已经超过了iOS数量,但是android的程序却远远没有iOS上那么优秀,市场也远远没有那么繁荣,除了android的开发难度会更大(虽然JAVA容易,但是设备太杂),最大的原因其实大家都知道,因为apple培养了很多年的用户付费习惯,这是虽然有不亚于apple fans数量的,但是长期提供免费服务的google所最欠缺的,而,很现实的,能让开发者赚钱的地方,才有最多,最好的开发者。

    在中国,有的看起来还算懂经济的人甚至认为版权的保护是不重要的,认为应该自由,我看来非常的悲哀。。。。。无论是软件,电影,音乐,厂商没有利益,你甚至根本享受不到,而版权,咋看起来,那是保护厂商利益与用户作对的,但是事实上,也保护了用户的利益,因为只有他们存在盈利的可能,你才能有更好的软件可用。不要拿几个小小的特例来说事,(比如Unix的发展因为版权问题受到一些阻碍啥的),你能不能够想象一个没有版权保护的世界?

    还有观点认为版权啥的那是支撑着厂商暴利的东西,需要强烈反对,其实,这个世界暴利的东西只有权利,市场上实在太少,能够存在的时间也太短了,因为一旦暴利,其他人早就一拥而上了。而IT产品算是个比较特别的例子,我常常看到别人分析apple产品(比如iPhone)的成本,那就是将所有零部件的价格加起来,然后说apple实在暴利,其实我倒是不反对apple的东西比别的厂商利润高,但是这样计算的逻辑也实在太搞笑了,
那么软件行业就更特别了,
难怪有人同理提出windows的生产成本就3块钱,一个3块钱的东西,要中国人花几十块几百块去买,那就是天方夜谭了。其实这里有个小故事,我在gameloft工作时参与制作的spiderman,售价6.99美元,在app store 0.99就能买到很优秀的游戏时,这个售价还是比较高的了,所以我虽然的确第一时间购买,(仅仅因为自己开发过)但是还是和以前同事抱怨了一下,卖的好贵啊。。。。。同事的回答非常直接,看部电影还不几十块,这是消费观念的问题。的确,消费观念哪,在中国享受习惯了免费的软件,潜意识认为软件都可以免费用了,就算卖0.99美元(7块钱在中国其实还可以买什么?)都会觉得贵的。

    同理,为啥中国有着还算发达的软件业但是却几乎没有自己真正的好的软件产品/品牌,为啥今天中国的单机游戏,除了回合制RPG还是回合制RPG?(即使是回合制RPG都少了)很多人曾经说,中国的PC能够这么快的普及,盗版是个很重要的因素,因为降低了中国人使用PC的成本,所以上面其实是故意纵容的,但是其实从长久来看,损害的是整个中国的软件产业。从更大范围来看,也降低了很多国外厂商开发中文软件的兴趣,为啥那么多的软件,游戏有7,8国语言,但是却没有中文?因为这个原因,倒是助长了民间汉化的热情。其实总的来说,最后还是损害了用户自身的利益。

    有人反对仅仅谈及利益,所以有开源软件,但是同时,有人认为开源软件损害了真正软件开发者的利益,那是洪水猛兽。其实,当经济发展到一定地步,人类不愁吃,不愁穿了,才会有除了生产活动外的其它活动,比如娱乐,比如开源项目。大部分的开源项目,都还是以兴趣为基础的,而兴趣,不是连饭的吃不饱的人能够多想的,这也是中国很难有优秀的开源项目的原因。这也是为啥中国有的开发者(还在吃饭阶段)认为开源软件(发达国家已经脱离吃饭阶段了)损害了自己的利益。无论多少成功的开源项目最后创造了很大的经济效益(比如MySQL),那都仅仅是开源项目的特例而已。

    谈到开源,也许xmaks可以效仿blender,商业上失败,走开源的道路,起码最后还能活下来。。。。。

    其实写到这里,自己都不知道自己在写些什么了。。。。。仅仅作为看到XMarks死掉后的一次的感叹吧。


 

阅读全文....