OPhone 3D开发之解析渲染MS3D模型

OPhone 3D开发之解析渲染MS3D模型

 OPhone平台中,3D模块已经成为一项标准配置,而且随着硬件成本的降低,搭配硬件加速图形芯片 的移动设备也越来越多地出现在人们的视野当中,手机上的3D再也不是“幻灯片”的代名词。享受着快捷无比的3G网络,在你心爱的OPhone手机上玩着真 正的魔兽世界,这也许在不远的未来就会变成现实。本文将以解析渲染MS3D格式的3D模型为例子,介绍OPhone平台中使用OpenGL ES进行3D程序的开发。程序最终效果如图1所示:

图1 程序最终效果图

OPhone中的OpenGL ES简介
       OpenGL ES是免授权费的、跨平台的、功能完善的2D和3D图形应用程序接口API,它针对多种嵌入式系统专门设计,由精心定义的桌面OpenGL子集组成,创造 了软件与图形加速间灵活强大的底层交互接口。OPhone中目前提供基于OpenGL ES 1.X的应用程序接口,整体与Java ME中的JSR 239 OpenGL ES API类似,但相比之下更为强大和易用。

       OPhone中提供的 android.opengl.GLSurfaceView辅助类,进一步封装了OpenGL ES与底层视窗系统的交互,开发者可以很方便地进行创建OpenGL ES渲染窗口、重载按键触屏事件响应、设置渲染模式、配置EGL参数等操作。由于GLSurfaceView是独立于系统UI线程之外运行的,因此在系统 UI线程挂起或者恢复时,需要显式调用GLSurfaceView中的onPause()或者onResume()来通知底层OpenGL ES模块进行相应处理。下面的代码简单展示了GLSurfaceView的使用方法:

  1. public   class  MyGLSurfaceView  extends  GLSurfaceView {   
  2.      private   final   float  TOUCH_SCALE_FACTOR =  180 .0f /  320 ;   
  3.      /**  
  4.      * 具体实现的渲染器  
  5.      */   
  6.      private  OPhoneOglesDevRenderer mRenderer;   
  7.      /**  
  8.      * 记录上次触屏位置的坐标  
  9.      */   
  10.      private   float  mPreviousX, mPreviousY;   
  11.   
  12.      public  MyGLSurfaceView(Context context) {   
  13.          super (context);   
  14.          // 设置渲染器   
  15.         mRenderer =  new  OPhoneOglesDevRenderer(context);   
  16.         setRenderer(mRenderer);   
  17.          // 设置渲染模式为主动渲染   
  18.         setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);   
  19.     }   
  20.   
  21.      public   void  onPause() {   
  22.          super .onPause();   
  23.     }   
  24.        
  25.      public   void  onResume() {   
  26.          super .onResume();   
  27.     }      
  28.   
  29.      /**  
  30.      * 响应触屏事件  
  31.      */   
  32.      @Override   
  33.      public   boolean  onTouchEvent(MotionEvent e) {   
  34.          float  x = e.getX();   
  35.          float  y = e.getY();   
  36.          switch  (e.getAction()) {   
  37.          case  MotionEvent.ACTION_MOVE:   
  38.              float  dx = x - mPreviousX;   
  39.              float  dy = y - mPreviousY;   
  40.             mRenderer.mAngleX += dx * TOUCH_SCALE_FACTOR;   
  41.             mRenderer.mAngleY += dy * TOUCH_SCALE_FACTOR;   
  42.             requestRender();   
  43.         }   
  44.         mPreviousX = x;   
  45.         mPreviousY = y;   
  46.          return   true ;   
  47.     }   
  48. }  
public class MyGLSurfaceView extends GLSurfaceView { private final float TOUCH_SCALE_FACTOR = 180.0f / 320; /** * 具体实现的渲染器 */ private OPhoneOglesDevRenderer mRenderer; /** * 记录上次触屏位置的坐标 */ private float mPreviousX, mPreviousY; public MyGLSurfaceView(Context context) { super(context); // 设置渲染器 mRenderer = new OPhoneOglesDevRenderer(context); setRenderer(mRenderer); // 设置渲染模式为主动渲染 setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); } public void onPause() { super.onPause(); } public void onResume() { super.onResume(); } /** * 响应触屏事件 */ @Override public boolean onTouchEvent(MotionEvent e) { float x = e.getX(); float y = e.getY(); switch (e.getAction()) { case MotionEvent.ACTION_MOVE: float dx = x - mPreviousX; float dy = y - mPreviousY; mRenderer.mAngleX += dx * TOUCH_SCALE_FACTOR; mRenderer.mAngleY += dy * TOUCH_SCALE_FACTOR; requestRender(); } mPreviousX = x; mPreviousY = y; return true; } }


OpenGL ES开发简要框架
       开发OpenGL ES程序,首要做的就是设置视口,设置投影矩阵,设置模型视图矩阵等。对于设置模型视图矩阵,我们通常会分别设置相机矩阵和模型矩阵。对于一些全局性的设 置,我们通常只需要执行一次;而对于那些需要动态改变的属性,则应该在相应事件发生时或者逐帧进行动态更新。 GLSurfaceView.Renderer接口提供了监视绘图表面创建、改变以及逐帧更新的方法,分别是: 

  1. /**  
  2.      * 创建绘图表面时调用  
  3.      */   
  4.      @Override   
  5.      public   void  onSurfaceCreated(GL10 gl, EGLConfig config)   
  6. /**  
  7.      * 当绘图表面尺寸发生改变时调用  
  8.      */   
  9.      @Override   
  10.      public   void  onSurfaceChanged(GL10 gl,  int  width,  int  height)   
  11. /**  
  12.      * 逐帧渲染  
  13.      */   
  14.      @Override   
  15.      public   void  onDrawFrame(GL10 gl)   
/** * 创建绘图表面时调用 */ @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) /** * 当绘图表面尺寸发生改变时调用 */ @Override public void onSurfaceChanged(GL10 gl, int width, int height) /** * 逐帧渲染 */ @Override public void onDrawFrame(GL10 gl)

       通常,我们在onSurfaceCreated()中通过调用glHint()函数来设置渲染质量与速度的平衡,设置清屏颜色,着色模型,启用背面剪裁和深度测试,以及禁用光照和混合等全局性设置。相关代码如下:

  1. public   void  onSurfaceCreated(GL10 gl, EGLConfig config) {   
  2.          //全局性设置   
  3.         gl.glDisable(GL10.GL_DITHER);   
  4.            
  5.         gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);   
  6.          //设置清屏背景颜色   
  7.         gl.glClearColor( 0 .5f,  0 .5f,  0 .5f,  1 );   
  8.          //设置着色模型为平滑着色   
  9.         gl.glShadeModel(GL10.GL_SMOOTH);   
  10.            
  11.          //启用背面剪裁   
  12.         gl.glEnable(GL10.GL_CULL_FACE);   
  13.         gl.glCullFace(GL10.GL_BACK);   
  14.          //启用深度测试   
  15.         gl.glEnable(GL10.GL_DEPTH_TEST);   
  16.          //禁用光照和混合   
  17.         gl.glDisable(GL10.GL_LIGHTING);   
  18.         gl.glDisable(GL10.GL_BLEND);   
  19.     }  
public void onSurfaceCreated(GL10 gl, EGLConfig config) { //全局性设置 gl.glDisable(GL10.GL_DITHER); gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST); //设置清屏背景颜色 gl.glClearColor(0.5f, 0.5f, 0.5f, 1); //设置着色模型为平滑着色 gl.glShadeModel(GL10.GL_SMOOTH); //启用背面剪裁 gl.glEnable(GL10.GL_CULL_FACE); gl.glCullFace(GL10.GL_BACK); //启用深度测试 gl.glEnable(GL10.GL_DEPTH_TEST); //禁用光照和混合 gl.glDisable(GL10.GL_LIGHTING); gl.glDisable(GL10.GL_BLEND); }

       在onSurfaceChanged中,我们会根据绘图表面尺寸的改变,来即时改变视口大小,以及重新设置投影矩阵。相关代码如下:

  1. public   void  onSurfaceChanged(GL10 gl,  int  width,  int  height) {   
  2.          //设置视口   
  3.         gl.glViewport( 0 0 , width, height);   
  4.            
  5.          //设置投影矩阵   
  6.          float  ratio = ( float ) width / height; //屏幕宽高比   
  7.         gl.glMatrixMode(GL10.GL_PROJECTION);   
  8.         gl.glLoadIdentity();   
  9.         GLU.gluPerspective(gl,  45 .0f, ratio,  1 5000 );   
  10.          //每次修改完GL_PROJECTION后,最好将当前矩阵模型设置回GL_MODELVIEW   
  11.         gl.glMatrixMode(GL10.GL_MODELVIEW);   
  12.     }  
public void onSurfaceChanged(GL10 gl, int width, int height) { //设置视口 gl.glViewport(0, 0, width, height); //设置投影矩阵 float ratio = (float) width / height;//屏幕宽高比 gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity(); GLU.gluPerspective(gl, 45.0f, ratio, 1, 5000); //每次修改完GL_PROJECTION后,最好将当前矩阵模型设置回GL_MODELVIEW gl.glMatrixMode(GL10.GL_MODELVIEW); }

      在onDrawFrame中,需要编写的是每帧实际渲染的代码,包括清屏,设置模型视图矩阵,渲染模型,以及相应的update函数。相关代码如下:

  1. public   void  onDrawFrame(GL10 gl) {   
  2.          //一般的opengl程序,首先要做的就是清屏   
  3.         gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);   
  4.            
  5.          //紧接着设置模型视图矩阵   
  6.         setUpCamera(gl);   
  7.            
  8.          //渲染物体   
  9.         drawModel(gl);   
  10.            
  11.          //更新时间   
  12.         updateTime();    
  13.     }  
public void onDrawFrame(GL10 gl) { //一般的opengl程序,首先要做的就是清屏 gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); //紧接着设置模型视图矩阵 setUpCamera(gl); //渲染物体 drawModel(gl); //更新时间 updateTime(); }

        设置模型视图矩阵(即GL_MODELVIEW矩阵)时,我们通常分别设置相机和物体矩阵。在设置相机矩阵时,我们可以通过调用
GLU.gluLookAt (GL10 gl, float eyeX, float eyeY, float eyeZ, float centerX, float centerY, float centerZ, float upX, float upY, float upZ) 传入视点位置(eyeX, eyeY, eyeZ)、被观察体的中心位置(centerX, centerY, centerZ)以及相机向上方向的向量(upX, upY, upZ)。相关代码如下:

  1. /**  
  2.      * 设置相机矩阵  
  3.      * @param gl  
  4.      */   
  5.      private   void  setUpCamera(GL10 gl) {   
  6.         gl.glMatrixMode(GL10.GL_MODELVIEW);   
  7.         gl.glLoadIdentity();   
  8.         GLU.gluLookAt(gl, mfEyeX, mfEyeY, mfEyeZ, mfCenterX, mfCenterY, mfCenterZ,  0 1 0 );   
  9.     }  
/** * 设置相机矩阵 * @param gl */ private void setUpCamera(GL10 gl) { gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); GLU.gluLookAt(gl, mfEyeX, mfEyeY, mfEyeZ, mfCenterX, mfCenterY, mfCenterZ, 0, 1, 0); }

          OpenGL ES中采用的是矩阵堆栈体系。对于模型视图矩阵,堆栈深度至少为16;对于投影矩阵或者纹理矩阵,则至少为2。由于OpenGL ES中的矩阵操作,都是针对当前栈顶的矩阵,因此很多时候需要配对使用glPushMatrix()和glPopMatrix()来进行保存和恢复矩阵现 场。在本例中,渲染模型之前,我们首先使用glPushMatrix()来复制当前模型视图矩阵,并将其推入到栈顶,之后所有的矩阵操作均针对该矩阵。然 后我们通过调用glRotate()函数,进行适当的旋转,在渲染模型完毕之后,通过调用glPopMatrix()将当前矩阵弹出,恢复之前的矩阵现 场。相关代码如下:

  1. /**  
  2.      * 渲染模型  
  3.      * @param gl  
  4.      */   
  5.      private   void  drawModel(GL10 gl) {   
  6.         gl.glPushMatrix();   
  7.         {      
  8.              //首先对模型进行旋转   
  9.             gl.glRotatef(mfAngleX,  1 0 0 ); //绕X轴旋转   
  10.             gl.glRotatef(mfAngleY,  0 1 0 ); //绕Y轴旋转   
  11.              if (mModel.containsAnimation()) {   
  12.                  //如果模型有动画,那么按时间就更新动画   
  13.                  if  (mMsPerFrame >  0 ) {   
  14.                     mModel.animate(mMsPerFrame *  0 .001f); //将毫秒数转化为秒, /1000   
  15.                 }   
  16.                 mModel.fillRenderBuffer(); //更新顶点缓存   
  17.             }   
  18.             mModel.render(gl); //渲染模型   
  19.             mModel.renderJoints(gl); //渲染关节,骨骼   
  20.         }   
  21.         gl.glPopMatrix();   
  22.     }  
/** * 渲染模型 * @param gl */ private void drawModel(GL10 gl) { gl.glPushMatrix(); { //首先对模型进行旋转 gl.glRotatef(mfAngleX, 1, 0, 0);//绕X轴旋转 gl.glRotatef(mfAngleY, 0, 1, 0);//绕Y轴旋转 if(mModel.containsAnimation()) { //如果模型有动画,那么按时间就更新动画 if (mMsPerFrame > 0) { mModel.animate(mMsPerFrame * 0.001f);//将毫秒数转化为秒, /1000 } mModel.fillRenderBuffer();//更新顶点缓存 } mModel.render(gl);//渲染模型 mModel.renderJoints(gl);//渲染关节,骨骼 } gl.glPopMatrix(); }

       OpenGL ES中支持三种渲染图元:点(GL_POINTS)、线(GL_LINES)和三角形(GL_TRIANGLES)。在本例子中,模型实体采用三角形渲染 (对应函数mModel.render(gl)),而对于有骨骼信息的模型,会使用点和线来渲染骨骼辅助信息(对应函数 mModel.renderJoints(gl))。OpenGL ES抛弃了OpenGL中传统但低效的glBegin()、glEnd()的渲染方式,采用了更为高效的批量渲染模式,使用 java.nio.Buffer对象来存储渲染数据,之后通过调用glVertexPointer()、glNormalPointer()、 glColorPointer()以及glTextureCoordPointer()传入Buffer对象来分别设置顶点位置、法线、颜色和纹理坐标渲 染数据。在设置渲染数据的同时,需要通过调用glEnableClientState()函数,分别传入GL_VERTEX_ARRAY、 GL_NORMAL_ARRAY、GL_COLOR_ARRAY和GL_TEXTURE_COORD_ARRAY来通知底层引擎启用相应渲染属性数据。这 四个渲染属性并非要全部设置,而是可以根据需要只是启用其中的某几个。在本例中,渲染模型实体时,仅启用了顶点位置数据和纹理坐标数据;在渲染点线的骨骼 辅助信息时,则仅仅启用了顶点位置数据。对于那些没有被启用的渲染属性,必须要确保其当前处于为非活动状态(即调用 glDisableClientState()),否则就可能会对渲染结果造成一定影响,或者白白加重底层管线运算负担。

        另外需要注意的是OPhone中要传入gl*Pointer()函数的Buffer对象必须要为direct模式申请的,这样可以确保缓存对象放置在 Native的堆中,以免受到Java端的垃圾回收机制的影响。对于FloatBuffer、ShortBuffer和IntBuffer等多字节的缓存 对象,它们的字节顺序必须设置为nativeOrder,否则会极大降低程序执行效率。

       在设置好各个渲染属性的数据之后,就要通过调用glDrawArrays()或者glDrawElements()来进行数据的最终 提交渲染。前者表示传入的数据是最终要渲染的数据,可以直接渲染,而后者会根据传入的索引,由底层重组最终要真正渲染的数据。相比之下,后者可以节省更多 的内存。下面的代码展示了以三角形来渲染模型实体,启用顶点位置数据和纹理坐标数据,未启用法线和颜色数据:

  1. /**  
  2.      * 渲染实体模型  
  3.      * @param gl  
  4.      */   
  5.      public   void  render(GL10 gl) {   
  6.         gl.glPushMatrix();   
  7.         {   
  8.              //设置默认颜色   
  9.             gl.glColor4f( 1 .0f,  0 .5f,  0 .5f,  1 .0f);   
  10.                
  11.              //启用客户端状态   
  12.             gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);   
  13.                
  14.              //遍历所有的MS3D Group,渲染每一个Group   
  15.              for  ( int  i =  0 ; i < mpGroups.length; i++) {   
  16.                  if  (mpGroups[i].getTriangleCount() ==  0 ) {   
  17.                      //如果该Group包含的三角形个数为零,则直接跳过   
  18.                      continue ;   
  19.                 }   
  20.                  //得到相应纹理   
  21.                 TextureInfo tex = mpTexInfo[i % mpTexInfo.length];   
  22.                    
  23.                  if  (tex !=  null ) {   
  24.                      //如果纹理不为空,则绑定相应纹理   
  25.                     gl.glBindTexture(GL10.GL_TEXTURE_2D, tex.mTexID);   
  26.                      //启用纹理贴图   
  27.                     gl.glEnable(GL10.GL_TEXTURE_2D);   
  28.                      //绑定纹理坐标数据   
  29.                     gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);   
  30.                     gl.glTexCoordPointer( 2 , GL10.GL_FLOAT,  0 ,   
  31.                             mpBufTextureCoords[i]);   
  32.   
  33.                 }  else  {   
  34.                      //如果纹理为空,禁用纹理贴图   
  35.                      //禁用纹理客户端状态   
  36.                     gl.glDisable(GL10.GL_TEXTURE_2D);   
  37.                     gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);   
  38.                 }   
  39.                  //绑定顶点数据   
  40.                 gl.glVertexPointer( 3 , GL10.GL_FLOAT,  0 , mpBufVertices[i]);   
  41.                  //提交渲染   
  42.                 gl.glDrawArrays(GL10.GL_TRIANGLES,  0 , mpGroups[i]   
  43.                         .getTriangleCount() *  3 );   
  44.             }   
  45.              //渲染完毕,重置客户端状态   
  46.             gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);   
  47.             gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);   
  48.             gl.glDisable(GL10.GL_TEXTURE_2D);   
  49.         }   
  50.         gl.glPopMatrix();   
  51.     }  
/** * 渲染实体模型 * @param gl */ public void render(GL10 gl) { gl.glPushMatrix(); { //设置默认颜色 gl.glColor4f(1.0f, 0.5f, 0.5f, 1.0f); //启用客户端状态 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); //遍历所有的MS3D Group,渲染每一个Group for (int i = 0; i < mpGroups.length; i++) { if (mpGroups[i].getTriangleCount() == 0) { //如果该Group包含的三角形个数为零,则直接跳过 continue; } //得到相应纹理 TextureInfo tex = mpTexInfo[i % mpTexInfo.length]; if (tex != null) { //如果纹理不为空,则绑定相应纹理 gl.glBindTexture(GL10.GL_TEXTURE_2D, tex.mTexID); //启用纹理贴图 gl.glEnable(GL10.GL_TEXTURE_2D); //绑定纹理坐标数据 gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mpBufTextureCoords[i]); } else { //如果纹理为空,禁用纹理贴图 //禁用纹理客户端状态 gl.glDisable(GL10.GL_TEXTURE_2D); gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); } //绑定顶点数据 gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mpBufVertices[i]); //提交渲染 gl.glDrawArrays(GL10.GL_TRIANGLES, 0, mpGroups[i] .getTriangleCount() * 3); } //渲染完毕,重置客户端状态 gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisable(GL10.GL_TEXTURE_2D); } gl.glPopMatrix(); }


         程序中渲染骨骼关节辅助信息的部分,就是以点和线的模型进行渲染,相关代码如下

 

 

  1. /**  
  2.      * 渲染骨骼帮助信息  
  3.      * @param gl  
  4.      */   
  5.      public   void  renderJoints(GL10 gl) {   
  6.          if (!containsJoint()) {   
  7.              return ;   
  8.         }   
  9.          //为保证骨骼始终可见,暂时禁用深度测试   
  10.         gl.glDisable(GL10.GL_DEPTH_TEST);   
  11.          //设置点和线的宽度   
  12.         gl.glPointSize( 4 .0f);   
  13.         gl.glLineWidth( 2 .0f);   
  14.          //仅仅启用顶点数据   
  15.         gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);   
  16.            
  17.          //渲染骨骼连线   
  18.         gl.glColor4f( 1 .0f,  0 .0f,  0 .0f,  1 .0f); //设置颜色   
  19.         gl.glVertexPointer( 3 , GL10.GL_FLOAT,  0 , mBufJointLinePosition);   
  20.          //提交渲染   
  21.         gl.glDrawArrays(GL10.GL_LINES,  0 , mJointLineCount);   
  22.            
  23.          //渲染关节点   
  24.         gl.glColor4f( 1 .0f,  1 .0f,  0 .0f,  1 .0f); //设置颜色   
  25.         gl.glVertexPointer( 3 , GL10.GL_FLOAT,  0 , mBufJointPointPosition);   
  26.          //提交渲染   
  27.         gl.glDrawArrays(GL10.GL_POINTS,  0 , mJointPointCount);   
  28.            
  29.          //重置回默认状态   
  30.         gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);   
  31.         gl.glPointSize( 1 .0f);   
  32.         gl.glLineWidth( 1 .0f);   
  33.         gl.glEnable(GL10.GL_DEPTH_TEST);   
  34.     }  
/** * 渲染骨骼帮助信息 * @param gl */ public void renderJoints(GL10 gl) { if(!containsJoint()) { return; } //为保证骨骼始终可见,暂时禁用深度测试 gl.glDisable(GL10.GL_DEPTH_TEST); //设置点和线的宽度 gl.glPointSize(4.0f); gl.glLineWidth(2.0f); //仅仅启用顶点数据 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); //渲染骨骼连线 gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f);//设置颜色 gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufJointLinePosition); //提交渲染 gl.glDrawArrays(GL10.GL_LINES, 0, mJointLineCount); //渲染关节点 gl.glColor4f(1.0f, 1.0f, 0.0f, 1.0f);//设置颜色 gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufJointPointPosition); //提交渲染 gl.glDrawArrays(GL10.GL_POINTS, 0, mJointPointCount); //重置回默认状态 gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glPointSize(1.0f); gl.glLineWidth(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); }

纹理操作
        在前面的代码中,我们看到了启用、绑定纹理等操作。纹理映射是3D中非常重要的一块,如果没有纹理,整个3D世界就会只是一些单纯的色块。OPhone中 目前支持2D纹理映射(贴图尺寸必须要为2的N次方),并支持2个以上的纹理贴图单元。由于纹理数据存储在OpenGL ES服务器端(可以理解为GPU端,即Graphics Process Unit,图形处理单元),因此需要我们从客户端(即外部的应用程序端)将像素数据传入,由底层将这些像素转换成更为高效的、对硬件更为友好的纹素格式。 OpenGL ES中的每一个纹理都被当作一个纹理对象,它除了包括纹理像素数据之外,还包括该纹理的其他属性,比如名字、过滤模式、混合模式等。开发者需要首先向底层 申请一个纹理名称,之后上传纹理像素数据,以及设置其他属性。下面的代码向我们展示了如何在OPhone中创建一个纹理对象:

  1. /**  
  2.      * 创建一个纹理对象  
  3.      * @param context - 应用程序环境  
  4.      * @param gl - opengl es对象  
  5.      * @param resID - R.java中的资源ID  
  6.      * @param wrap_s_mode - 纹理环绕S模式  
  7.      * @param wrap_t_mode - 纹理环绕T模式  
  8.      * @return 申请好的纹理ID  
  9.      */   
  10.      public   static   int  getTexture(Context context, GL10 gl,  int  resID,   
  11.              int  wrap_s_mode,  int  wrap_t_mode) {   
  12.          //申请一个纹理对象ID   
  13.          int [] textures =  new   int [ 1 ];   
  14.         gl.glGenTextures( 1 , textures,  0 );   
  15.          //绑定这个申请来的ID为当前纹理操作对象   
  16.          int  textureID = textures[ 0 ];   
  17.         gl.glBindTexture(GL10.GL_TEXTURE_2D, textureID);   
  18.          //设置当前纹理对象的过滤模式   
  19.         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,   
  20.                 GL10.GL_NEAREST);   
  21.         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,   
  22.                 GL10.GL_LINEAR);   
  23.          //设置环绕模式   
  24.         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,   
  25.                 wrap_s_mode);   
  26.         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,   
  27.                 wrap_t_mode);   
  28.          //设置混合模式   
  29.         gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,   
  30.                 GL10.GL_REPLACE);   
  31.            
  32.          //开始载入纹理   
  33.         InputStream is = context.getResources().openRawResource(resID);   
  34.         Bitmap bitmap;   
  35.          try  {   
  36.             bitmap = BitmapFactory.decodeStream(is);   
  37.         }  finally  {   
  38.              try  {   
  39.                 is.close();   
  40.             }  catch  (IOException e) {   
  41.                  // Ignore.   
  42.             }   
  43.         }   
  44.            
  45.          //绑定像素数据到纹理对象   
  46.         GLUtils.texImage2D(GL10.GL_TEXTURE_2D,  0 , bitmap,  0 );   
  47.         bitmap.recycle();   
  48.   
  49.          return  textureID;   
  50.     }  
/** * 创建一个纹理对象 * @param context - 应用程序环境 * @param gl - opengl es对象 * @param resID - R.java中的资源ID * @param wrap_s_mode - 纹理环绕S模式 * @param wrap_t_mode - 纹理环绕T模式 * @return 申请好的纹理ID */ public static int getTexture(Context context, GL10 gl, int resID, int wrap_s_mode, int wrap_t_mode) { //申请一个纹理对象ID int[] textures = new int[1]; gl.glGenTextures(1, textures, 0); //绑定这个申请来的ID为当前纹理操作对象 int textureID = textures[0]; gl.glBindTexture(GL10.GL_TEXTURE_2D, textureID); //设置当前纹理对象的过滤模式 gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); //设置环绕模式 gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, wrap_s_mode); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, wrap_t_mode); //设置混合模式 gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_REPLACE); //开始载入纹理 InputStream is = context.getResources().openRawResource(resID); Bitmap bitmap; try { bitmap = BitmapFactory.decodeStream(is); } finally { try { is.close(); } catch (IOException e) { // Ignore. } } //绑定像素数据到纹理对象 GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0); bitmap.recycle(); return textureID; }


      创建好纹理对象之后,在使用时,需要首先通过调用gl.glEnable(GL10.GL_TEXTURE_2D)来通知底层开启纹理贴图操作,之后绑定 相应的纹理ID到当前纹理贴图单元,同时通过调用glTexCoordPointer()来设置好相应的纹理坐标信息,最终提交渲染时,底层就会自动进行 纹理映射操作。当纹理不再被使用时,可以通过调用glDeleteTextures()来将其删除。

输入事件响应
       我们可以重载GLSurfaceView的onTouchEvent()方法,从而监测用户对屏幕的触摸事件。本例中,我们根据触摸位置的改变,来对模型 进行绕Y轴和X轴的旋转。如果有需要,开发者还可以重载键盘按键onKeyDown()方法。值得注意的是,由于这些事件和渲染线程是分别独立的线程,因 此有些操作如果需要确保在渲染线程内部执行的话,可以调用queueEvent (Runnable)来将该操作附加到渲染线程操作队列中。相关代码如下:

  1. /**  
  2.      * 响应触屏事件  
  3.      */   
  4.      @Override   
  5.      public   boolean  onTouchEvent(MotionEvent e) {   
  6.          float  x = e.getX();   
  7.          float  y = e.getY();   
  8.          switch  (e.getAction()) {   
  9.          case  MotionEvent.ACTION_MOVE:   
  10.              float  dx = x - mPreviousX;   
  11.              float  dy = y - mPreviousY;   
  12.             mRenderer.mfAngleY += dx * TOUCH_SCALE_FACTOR;   
  13.             mRenderer.mfAngleX += dy * TOUCH_SCALE_FACTOR;   
  14.             requestRender();   
  15.         }   
  16.         mPreviousX = x;   
  17.         mPreviousY = y;   
  18.          return   true ;   
  19.     }  
/** * 响应触屏事件 */ @Override public boolean onTouchEvent(MotionEvent e) { float x = e.getX(); float y = e.getY(); switch (e.getAction()) { case MotionEvent.ACTION_MOVE: float dx = x - mPreviousX; float dy = y - mPreviousY; mRenderer.mfAngleY += dx * TOUCH_SCALE_FACTOR; mRenderer.mfAngleX += dy * TOUCH_SCALE_FACTOR; requestRender(); } mPreviousX = x; mPreviousY = y; return true; }


MS3D模型的解析和渲染
          MilkShape3D是一款轻量级的3D建模软件,最早是为《半条命》游戏的模型制作而开发的,随着越来越多的特性的添加,其功能也逐渐强大,开发者可以从其官方网站(http://chumbalum.swissquake.ch/ ) 获得评估版本。它自带的模型格式为MS3D格式,包括二进制版本和TXT版本,本例中我们解析的是二进制版。MS3D格式简单紧凑,支持骨骼动画,非常适 合初学者学习和使用。MS3D格式由头信息(Header)、顶点数据(Vertices)、三角形数据(Triangles)、分组信息 (Groups)、材质信息(Materials)、骨骼关节数据(Joints)以及动画播放信息等部分组成。载入时,根据模型格式白皮书,读取并解析 相应数据即可。需要注意的是由于MS3D中默认数据字节序为Little-Endian,因此我们构建了一个专门的 LittleEndianDataInputStream来确保不同平台下Java端数据的正确读取。由于篇幅关系,这里将不详细介绍模型载入模块,具体 请参考附带源码,这里仅介绍下如何调用现有代码读取一个MS3D模型:

 

  1. /**  
  2.      * 载入模型  
  3.      * @param gl  
  4.      * @param idxModel - 模型资源索引  
  5.      * @param pIdxTex - 纹理数组  
  6.      */   
  7.      private   void  loadModel(GL10 gl,  int  idxModel,  int [] pIdxTex) {   
  8.          try  {   
  9.             TextureInfo[] pTexInfos =  new  TextureInfo[pIdxTex.length];   
  10.             mModel =  new  IMS3DModel();   
  11.                
  12.              //打开模型二进制流   
  13.             InputStream is = mContext.getResources().openRawResource(idxModel);   
  14.                
  15.              if (mModel.loadModel(is)) {   
  16.                  //载入模型成功,开始载入纹理   
  17.                  for ( int  i =  0 ; i < pTexInfos.length; i++) {   
  18.                     pTexInfos[i] =  new  TextureInfo();   
  19.                      //得到创建成功的纹理对象名称   
  20.                     pTexInfos[i].mTexID = TextureFactory.getTexture(mContext, gl, pIdxTex[i]);   
  21.                 }   
  22.                  //赋予纹理   
  23.                 mModel.setTexture(pTexInfos);   
  24.             }  else  {   
  25.                 System.out.println( "Load Model Failed. IdxModel:"  + idxModel);   
  26.             }   
  27.                
  28.             is.close();   
  29.         }  catch (Exception ex) {   
  30.             ex.printStackTrace();   
  31.         }   
  32.     }  
/** * 载入模型 * @param gl * @param idxModel - 模型资源索引 * @param pIdxTex - 纹理数组 */ private void loadModel(GL10 gl, int idxModel, int[] pIdxTex) { try { TextureInfo[] pTexInfos = new TextureInfo[pIdxTex.length]; mModel = new IMS3DModel(); //打开模型二进制流 InputStream is = mContext.getResources().openRawResource(idxModel); if(mModel.loadModel(is)) { //载入模型成功,开始载入纹理 for(int i = 0; i < pTexInfos.length; i++) { pTexInfos[i] = new TextureInfo(); //得到创建成功的纹理对象名称 pTexInfos[i].mTexID = TextureFactory.getTexture(mContext, gl, pIdxTex[i]); } //赋予纹理 mModel.setTexture(pTexInfos); } else { System.out.println("Load Model Failed. IdxModel:" + idxModel); } is.close(); } catch(Exception ex) { ex.printStackTrace(); } }

       在载入模型过程中,我们会计算模型初始绑定盒以及绑定球,以便当载入完成后将相机放置于合适位置从而完全观察到模型本身。下面的代码 展示了一种较通用的处理方式,将模型放置在(0,0,0)位置,计算好相机位置以及视点朝向位置后,调用GLU.lookAt()函数来计算模型视图矩 阵:

 

  1. mfEyeX = mModel.getSphereCenter().x;   
  2.         mfEyeY = mModel.getSphereCenter().y;   
  3.         mfEyeZ = mModel.getSphereCenter().z + mModel.getSphereRadius() *  2 .8f;   
  4.         mfCenterX = mfCenterZ =  0 ;   
  5.         mfCenterY = mfEyeY;   
  6. GLU.gluLookAt(gl, mfEyeX, mfEyeY, mfEyeZ, mfCenterX, mfCenterY, mfCenterZ,  0 1 0 );  
mfEyeX = mModel.getSphereCenter().x; mfEyeY = mModel.getSphereCenter().y; mfEyeZ = mModel.getSphereCenter().z + mModel.getSphereRadius() * 2.8f; mfCenterX = mfCenterZ = 0; mfCenterY = mfEyeY; GLU.gluLookAt(gl, mfEyeX, mfEyeY, mfEyeZ, mfCenterX, mfCenterY, mfCenterZ, 0, 1, 0);


       对于每个MS3D模型,都包含一个顶点池(包括了模型所有的顶点),一个三角形池(包括了模型所有的三角形),若干个模型分组(Group),一个材质池 (Material)以及骨骼关节池(Joints)等。一个模型可能会有多个分组,比如一个人物模型,头部可能会是一个单独的分组。每个分组需要单独渲 染,渲染一个MS3D模型,实际上就是渲染这个模型的所有分组。由于OPhone中的OpenGL ES是采用java.nio.Buffer的形式进行渲染的,因此我们需要为每个分组构建对应的Buffer数据,包括顶点位置缓存、顶点纹理坐标缓存、 顶点法线缓存以及顶点颜色缓存。一般来说,法线用于光照计算,顶点颜色用于同纹理的混合等特殊操作,本例中我们没有相关的需求,因此只是创建了顶点位置缓 存和顶点纹理坐标缓存。对于静态模型,这两种缓存只需要创建一次即可;而对于包含动画信息的模型,随着每帧时间更新,顶点的位置可能会随时改变,因此顶点 位置缓存也需要同时更新,而顶点纹理坐标数据,除非是启用了纹理动画,否则不需要实时更新。在填充顶点位置缓存时,根据每个模型分组 (MS3DGroup)内的三角形索引,找到对应的三角形(MS3DTriangle),然后再根据三角形内顶点的索引,找到对应的顶点 (MS3DVertex),从而得到顶点位置信息。如果模型包含骨骼动画信息,那么需要根据顶点相关联的骨骼索引和权重等信息进行蒙皮计算,得到当前时间 的顶点位置,然后写入到顶点位置缓存中。相关代码如下:

 

  1. /**  
  2.      * 填充渲染缓存数据  
  3.      */   
  4.      public   void  fillRenderBuffer() {   
  5.          if (!mbDirtFlag) {   
  6.              //如果模型数据没有更新,那么就无需重新填充   
  7.              return ;   
  8.         }   
  9.         Vector3f position =  null ;   
  10.          //遍历所有Group   
  11.          for  ( int  i =  0 ; i < mpGroups.length; i++) {   
  12.              //获得该Group内所有的三角形索引   
  13.              int [] indexes = mpGroups[i].getTriangleIndicies();   
  14.             mpBufVertices[i].position( 0 );   
  15.              int  vertexIndex =  0 ;   
  16.              //遍历每一个三角形   
  17.              for  ( int  j =  0 ; j < indexes.length; j++) {   
  18.                  //从三角形池内找到对应三角形   
  19.                 MS3DTriangle triangle = mpTriangles[indexes[j]];   
  20.                  //遍历三角形的每个顶点   
  21.                  for  ( int  k =  0 ; k <  3 ; k++) {   
  22.                      //从顶点池中找到相应顶点   
  23.                     MS3DVertex vertex = mpVertices[triangle   
  24.                             .getVertexIndicies()[k]];   
  25.                      //获得最新的位置   
  26.                      //如果模型带骨骼,那么就是当前的变换后的位置   
  27.                      //否则就是初始位置   
  28.                      //具体的变换过程请参考animate(float timedelta)函数   
  29.                     position = vertex.mvTransformedLocation;   
  30.                      //填充顶点位置信息到缓存中   
  31.                     mpBufVertices[i].put(position.x);   
  32.                     mpBufVertices[i].put(position.y);   
  33.                     mpBufVertices[i].put(position.z);   
  34.                 }   
  35.             }   
  36.   
  37.             mpBufVertices[i].position( 0 );   
  38.         }   
  39.   
  40.         mbDirtFlag =  false ;   
  41.     }  
/** * 填充渲染缓存数据 */ public void fillRenderBuffer() { if(!mbDirtFlag) { //如果模型数据没有更新,那么就无需重新填充 return; } Vector3f position = null; //遍历所有Group for (int i = 0; i < mpGroups.length; i++) { //获得该Group内所有的三角形索引 int[] indexes = mpGroups[i].getTriangleIndicies(); mpBufVertices[i].position(0); int vertexIndex = 0; //遍历每一个三角形 for (int j = 0; j < indexes.length; j++) { //从三角形池内找到对应三角形 MS3DTriangle triangle = mpTriangles[indexes[j]]; //遍历三角形的每个顶点 for (int k = 0; k < 3; k++) { //从顶点池中找到相应顶点 MS3DVertex vertex = mpVertices[triangle .getVertexIndicies()[k]]; //获得最新的位置 //如果模型带骨骼,那么就是当前的变换后的位置 //否则就是初始位置 //具体的变换过程请参考animate(float timedelta)函数 position = vertex.mvTransformedLocation; //填充顶点位置信息到缓存中 mpBufVertices[i].put(position.x); mpBufVertices[i].put(position.y); mpBufVertices[i].put(position.z); } } mpBufVertices[i].position(0); } mbDirtFlag = false; }


MS3D的骨骼系统
        MS3D的一个顶点最多支持被4个骨骼所影响,这对于移动设备来讲已经够用了,实际上大部分顶点可能只被1个骨骼所影响。MS3DLib中骨骼相关的类有 MS3DJoint和Joint,前者用于模型数据的读取,后者用于实时骨骼计算,模型载入时读取MS3DJoint完毕后会自动转换为Joint来更方 便的进行计算。在Joint类中,存储着该骨骼关节的动画帧序列,包括每帧的位置偏移量和旋转量。当动画时间更新时,通过传入的时间查找到最接近的前后两 帧,之后根据插值得到当前时间的位置偏移量和旋转量合成变换矩阵(Matrix)。由于骨骼系统的层级关系,因此如果该关节有父节点,那么则需要乘以父关 节的矩阵,以得到该关节的最终矩阵。由于我们在载入模型关节数据时,已经确保了最上层的关节处于关节数组的前列,因此当后面的子关节更新时,它的父节点已 经被更新过,所以可以直接相乘。当所有骨骼关节的最终矩阵更新完毕之后,需要更新模型的全部顶点,根据每个顶点所关联的骨骼索引和权重信息,计算出顶点当 前时刻的位置信息,之后在fillRenderBuffer()函数中填入当前的顶点位置缓存以用于提交渲染。相关代码如下:

 

  1. /**  
  2.      * 根据时间来更新模型动画  
  3.      *   
  4.      * @param timedelta - 本次tick时间  
  5.      */   
  6.      public   void  animate( float  timedelta) {   
  7.          //累加时间   
  8.         mCurrentTime += timedelta;   
  9.   
  10.          if  (mCurrentTime > mTotalTime) {   
  11.             mCurrentTime =  0 .0f;   
  12.         }   
  13.          //首先要更新每个骨骼节点的当前位置信息   
  14.          for  ( int  i =  0 ; i < mpJoints.length; i++) {   
  15.             Joint joint = mpJoints[i];   
  16.              //如果不包含动画信息那就无需更新   
  17.              if  (joint.mNumTranslationKeyframes ==  0   
  18.                     && joint.mNumRotationKeyframes ==  0 ) {   
  19.                 joint.mMatGlobal.set(joint.mMatJointAbsolute);   
  20.                  continue ;   
  21.             }   
  22.   
  23.              //开始进行插值计算   
  24.              //首先进行旋转插值   
  25.             Matrix4f matKeyframe = getJointRotation(i, mCurrentTime);   
  26.              //进行偏移的线性插值   
  27.             matKeyframe.setTranslation(getJointTranslation(i, mCurrentTime));   
  28.              //乘以节点本身的相对矩阵   
  29.             matKeyframe.mul(joint.mMatJointRelative, matKeyframe);   
  30.                
  31.              //乘以父矩阵,得到最终矩阵   
  32.              if  (joint.mParentId == - 1 ) {   
  33.                 joint.mMatGlobal.set(matKeyframe);   
  34.             }  else  {   
  35.                 matKeyframe.mul(mpJoints[joint.mParentId].mMatGlobal,   
  36.                         matKeyframe);   
  37.                 joint.mMatGlobal.set(matKeyframe);   
  38.             }   
  39.         }   
  40.          //更新点线渲染的骨骼帮助信息   
  41.         updateJointsHelper();   
  42.   
  43.          //开始更新每个顶点   
  44.          for  ( int  i =  0 , n = mpVertices.length; i < n; i++) {   
  45.             MS3DVertex vertex = mpVertices[i];   
  46.   
  47.              if  (vertex.getBoneID() == - 1 ) {   
  48.                  //如果该顶点不受骨骼影响,那么就无需计算   
  49.                 vertex.mvTransformedLocation.set(vertex.getLocation());   
  50.             }  else  {   
  51.                  //通过骨骼运算,得到顶点的当前位置   
  52.                 transformVertex(vertex);   
  53.             }   
  54.         }   
  55.            
  56.         mbDirtFlag =  true ;   
  57.     }  
/** * 根据时间来更新模型动画 * * @param timedelta - 本次tick时间 */ public void animate(float timedelta) { //累加时间 mCurrentTime += timedelta; if (mCurrentTime > mTotalTime) { mCurrentTime = 0.0f; } //首先要更新每个骨骼节点的当前位置信息 for (int i = 0; i < mpJoints.length; i++) { Joint joint = mpJoints[i]; //如果不包含动画信息那就无需更新 if (joint.mNumTranslationKeyframes == 0 && joint.mNumRotationKeyframes == 0) { joint.mMatGlobal.set(joint.mMatJointAbsolute); continue; } //开始进行插值计算 //首先进行旋转插值 Matrix4f matKeyframe = getJointRotation(i, mCurrentTime); //进行偏移的线性插值 matKeyframe.setTranslation(getJointTranslation(i, mCurrentTime)); //乘以节点本身的相对矩阵 matKeyframe.mul(joint.mMatJointRelative, matKeyframe); //乘以父矩阵,得到最终矩阵 if (joint.mParentId == -1) { joint.mMatGlobal.set(matKeyframe); } else { matKeyframe.mul(mpJoints[joint.mParentId].mMatGlobal, matKeyframe); joint.mMatGlobal.set(matKeyframe); } } //更新点线渲染的骨骼帮助信息 updateJointsHelper(); //开始更新每个顶点 for (int i = 0, n = mpVertices.length; i < n; i++) { MS3DVertex vertex = mpVertices[i]; if (vertex.getBoneID() == -1) { //如果该顶点不受骨骼影响,那么就无需计算 vertex.mvTransformedLocation.set(vertex.getLocation()); } else { //通过骨骼运算,得到顶点的当前位置 transformVertex(vertex); } } mbDirtFlag = true; }


       在上面的插值运算中,位置偏移的插值和旋转的插值是分开处理的。对于以向量(x, y, z)表示的位移,这里我们使用的就是简单的线性插值方式,如果对插值质量要求更高、需要效果更平滑,可以采用Hermite样条插值等方式。对于关节的旋 转,这里是采用四元数表示的,四元数最大的优点就是便于球面插值,当我们利用四元数进行完插值计算之后,需要把四元数转换为相应的旋转矩阵。相关代码如 下:

 

  1. /**  
  2.      * 根据传入的时间,返回插值后的位置信息  
  3.      *   
  4.      * @param frames  
  5.      *            偏移量关键帧数组  
  6.      * @param time  
  7.      *            目标时间  
  8.      * @return 插值后的位置信息  
  9.      */   
  10.      private  Vector3f lerpKeyframeLinear(Keyframe[] frames,  float  time) {   
  11.          int  frameIndex =  0 ;   
  12.          int  numFrames = frames.length;   
  13.   
  14.          //这里可以使用二分查找进行优化   
  15.          while  (frameIndex < numFrames && frames[frameIndex].mfTime < time) {   
  16.             ++frameIndex;   
  17.         }   
  18.            
  19.          //首先处理边界情况   
  20.         Vector3f parameter = tmpVectorLerp;   
  21.          if  (frameIndex ==  0 ) {   
  22.             parameter.set(frames[ 0 ].mvParam.x, frames[ 0 ].mvParam.y,   
  23.                     frames[ 0 ].mvParam.z);   
  24.         }  else   if  (frameIndex == numFrames) {   
  25.             parameter.set(frames[numFrames -  1 ].mvParam.x,   
  26.                     frames[numFrames -  1 ].mvParam.y,   
  27.                     frames[numFrames -  1 ].mvParam.z);   
  28.         }  else  {   
  29.              int  prevFrameIndex = frameIndex -  1 ;   
  30.              //得到临近两帧   
  31.             Keyframe right = frames[frameIndex];   
  32.             Keyframe left = frames[prevFrameIndex];   
  33.              //计算插值因子   
  34.              float  timeDelta = right.mfTime - left.mfTime;   
  35.              float  interpolator = (time - left.mfTime) / timeDelta;   
  36.              //进行简单的线性插值   
  37.             parameter.interpolate(left.mvParam, right.mvParam, interpolator);   
  38.         }   
  39.   
  40.          return  parameter;   
  41.     }   
  42. /**  
  43.      * 根据传入的时间,计算插值后的旋转量  
  44.      *   
  45.      * @param frames  
  46.      *            旋转量关键帧数组  
  47.      * @param time  
  48.      *            目标时间  
  49.      * @return 插值后的旋转量数据  
  50.      */   
  51.      private  Quat4f lerpKeyframeRotate(Keyframe[] frames,  float  time) {   
  52.         Quat4f quat = tmpQuatLerp;   
  53.          int  frameIndex =  0 ;   
  54.          int  numFrames = frames.length;   
  55.            
  56.          //这里可以使用二分查找进行优化   
  57.          while  (frameIndex < numFrames && frames[frameIndex].mfTime < time) {   
  58.             ++frameIndex;   
  59.         }   
  60.          //首先处理边界情况   
  61.          if  (frameIndex ==  0 ) {   
  62.             quat.set(frames[ 0 ].mvParam);   
  63.         }  else   if  (frameIndex == numFrames) {   
  64.             quat.set(frames[numFrames -  1 ].mvParam);   
  65.         }  else  {   
  66.              int  prevFrameIndex = frameIndex -  1 ;   
  67.              //找到最邻近的两帧   
  68.             Keyframe right = frames[frameIndex];   
  69.             Keyframe left = frames[prevFrameIndex];   
  70.              //计算好插值因子   
  71.              float  timeDelta = right.mfTime - left.mfTime;   
  72.              float  interpolator = (time - left.mfTime) / timeDelta;   
  73.              //进行四元数插值   
  74.             Quat4f quatRight = tmpQuatLerpRight;   
  75.             Quat4f quatLeft = tmpQuatLerpLeft;   
  76.   
  77.             quatRight.set(right.mvParam);   
  78.             quatLeft.set(left.mvParam);   
  79.             quat.interpolate(quatLeft, quatRight, interpolator);   
  80.         }   
  81.   
  82.          return  quat;   
  83.     }  
/** * 根据传入的时间,返回插值后的位置信息 * * @param frames * 偏移量关键帧数组 * @param time * 目标时间 * @return 插值后的位置信息 */ private Vector3f lerpKeyframeLinear(Keyframe[] frames, float time) { int frameIndex = 0; int numFrames = frames.length; //这里可以使用二分查找进行优化 while (frameIndex < numFrames && frames[frameIndex].mfTime < time) { ++frameIndex; } //首先处理边界情况 Vector3f parameter = tmpVectorLerp; if (frameIndex == 0) { parameter.set(frames[0].mvParam.x, frames[0].mvParam.y, frames[0].mvParam.z); } else if (frameIndex == numFrames) { parameter.set(frames[numFrames - 1].mvParam.x, frames[numFrames - 1].mvParam.y, frames[numFrames - 1].mvParam.z); } else { int prevFrameIndex = frameIndex - 1; //得到临近两帧 Keyframe right = frames[frameIndex]; Keyframe left = frames[prevFrameIndex]; //计算插值因子 float timeDelta = right.mfTime - left.mfTime; float interpolator = (time - left.mfTime) / timeDelta; //进行简单的线性插值 parameter.interpolate(left.mvParam, right.mvParam, interpolator); } return parameter; } /** * 根据传入的时间,计算插值后的旋转量 * * @param frames * 旋转量关键帧数组 * @param time * 目标时间 * @return 插值后的旋转量数据 */ private Quat4f lerpKeyframeRotate(Keyframe[] frames, float time) { Quat4f quat = tmpQuatLerp; int frameIndex = 0; int numFrames = frames.length; //这里可以使用二分查找进行优化 while (frameIndex < numFrames && frames[frameIndex].mfTime < time) { ++frameIndex; } //首先处理边界情况 if (frameIndex == 0) { quat.set(frames[0].mvParam); } else if (frameIndex == numFrames) { quat.set(frames[numFrames - 1].mvParam); } else { int prevFrameIndex = frameIndex - 1; //找到最邻近的两帧 Keyframe right = frames[frameIndex]; Keyframe left = frames[prevFrameIndex]; //计算好插值因子 float timeDelta = right.mfTime - left.mfTime; float interpolator = (time - left.mfTime) / timeDelta; //进行四元数插值 Quat4f quatRight = tmpQuatLerpRight; Quat4f quatLeft = tmpQuatLerpLeft; quatRight.set(right.mvParam); quatLeft.set(left.mvParam); quat.interpolate(quatLeft, quatRight, interpolator); } return quat; }

       有关矩阵、四元数以及向量等数学知识背景,请参考相关3D数学基础书籍,这里限于篇幅不做过多的介绍。

程序介绍
       在本程序中,附带了若干个MS3D模型,有静态的,也有带骨骼动画的,如图2所示。

图2 内置MS3D模型列表

        在选项中可以设置是否渲染骨骼节点辅助信息,以及是否自动播放动画,见图3。当进入模型渲染界面后,可以通过触屏拖拉以旋转模型。

        本程序所有的源码都会发布在ophonesdn上,项目主页:http://www.ophonesdn.com/projectDetail/show/382

        开发者联系方式(MSN&EMail): xueyong@live.com


图3 程序选项界面

更多截图

总结
      本文通过对MS3D模型的解析和渲染,向大家介绍了OPhone平台下使用OpenGL ES进行3D开发的基本概念以及输入事件响应,同时也介绍了3D中的骨骼、动画等高级话题。这样就构成了一个小型的OPhone 3D程序开发框架,读者可以根据自己的需要对其进行进一步的完善。

作者介绍
        薛永,专注于移动平台3D应用程序的开发,熟悉M3G,JSR 239,OpenGL ES(OPhone&iphone)等多种移动3D开发平台。目前正在自主开发全套3D引擎,包括PC端场景/模型/动画/UI编辑器,3ds max导出插件,面向Java、C++的客户端。同时在制作一款3D射击游戏,到时会面向OPhone、iphone等多个平台发布。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值