Android上面三维封面

  在上一篇介绍OPhone 3D开发的文章《OPhone 3D开发之解析渲染MS3D模型》中,我们向大家介绍了如何使用OPhone中的OpenGL ES API开发一个基本的3D应用程序,本文将在前文的基础上,通过编写一个3D封面效果演示,进一步介绍OPhone平台中的众多3D特性。程序最终的效果图如下:

 

 

 

 

 

 

构建封面渲染几何体
       在上面的效果图中,大家可以看到每个封面,均由封面正面、倒影两部分组成。正面部分显示的是封面图片主体,倒影部分则是相对于正面做了一个垂直方向的反转,并且随着高度的下降透明度逐渐降低,最终与背景完全融合,从而营造出一种晶莹剔透的镜面倒影效果。

      这里的每个封面几何体渲染对象,实际上就是一个2D的矩形面片,最简单的方式,当然就是根据4个顶点,构建两个三角形进行渲染。但这会造成什么问题呢?请看下图:

 

 

          为了帮助大家更好的看清细节,下面是图2局部放大400%之后的截图:

       从上面的图2和图3可以看出,如果仅仅是用4个顶点,2个三角形去简单渲染封面,最终效果会很差,边缘锯齿会很明显。在移动设备上,要实现抗锯齿,除了显卡硬件设备支持之外,我们可以用一些取巧的方式,以较小的代价来模拟实现抗锯齿的效果。下面就将介绍常用的一种方法:边缘半透明混合重绘。

      这项技术的基本思想,就是在渲染完毕主体图片之后,对封面的边缘进行一次“描边”,使用Alpha过渡技术,通过半透明的像素将图片边缘像素与背景像素相融合,以达到模拟抗锯齿效果。为了启用纹理与顶点色的融合,需要我们通过调用glTexEnvf ( GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,GL10.GL_ MODULATE),将纹理对象的纹理模式设置为GL_MODULATE。这样我们就可以得到如图4的效果:

 

      可以看到,在多了一圈半透明的过渡边缘之后,整体锯齿感大大下降,整体效果可以见图1。为了达到这样的效果,我们需要在构建封面渲染几何体时,进行进一步的细分,把封面几何体分为主体部分和边缘部分。具体如图5所示:

          新的几何体由16个顶点构成9个四边形(即18个三角形),其中,由顶点5,6,9,10构成的中心四边形作为封面主体,顶点色的透明度设置为完全不透明(即Alpha = 1.0);而其余的四边形作为过渡边缘进行二次描边操作,剩余顶点的顶点色透明度均设置为完全透明(即Alpha = 0.0)。这样,从中心区域到边缘,就会是一个平滑淡出的效果。

         由于我们的每一个封面实际上是由两个组成,一个正面,一个倒影,因此,每一个封面实际上需要16 x 2 = 32个顶点,也就是前16个顶点表示封面的正面,后16个顶点表示封面倒影。然后我们再根据需要,对正面和倒影的顶点分别设置它们的位置、顶点色、纹理坐标等属性。创建全部顶点的相关代码如下:

  1. public   void  createCover() {   
  2.          //封面有16个顶点,这里还包括它自身 的倒影,所以一共要创建32个   
  3.         Vertex[] pVertices =  new  Vertex[ 16  *  2 ];   
  4.            
  5.          // 初始化操作   
  6.          for  ( int  i =  0 ; i < pVertices.length; i++) {   
  7.             pVertices[i] =  new  Vertex();   
  8.         }   
  9.   
  10.          // 设置封面初始化参数,宽度高度均为6.0f, 封面距离倒影的距离是0   
  11.          // 如果需要显示宽屏的图片,可以在这里修改图片比例   
  12.          float  width =  6 .0f, height =  6 .0f, heightFromMirror =  0 .0f;   
  13.          float  dim =  0 .5f;  // as uv   
  14.          float  mfBorderFraction =  0 .05f; //过渡边缘宽度系数,如果设置为0就表示没有边缘过渡   
  15.          float  dimLess =  0 .5f - ( 0 .5f * mfBorderFraction);   
  16.          //设置顶点的法线,这里由于没有启用光照,因此并未特别设置   
  17.         Vector3f normal =  new  Vector3f( 0 .0f,  1 .0f,  0 .0f);   
  18.          //设置顶点的位置   
  19.         pVertices[ 0 ].p.set(-dim, dim,  0 );   
  20.         pVertices[ 1 ].p.set(-dimLess, dim,  0 );   
  21.         pVertices[ 2 ].p.set(dimLess, dim,  0 );   
  22.         pVertices[ 3 ].p.set(dim, dim,  0 );   
  23.         pVertices[ 4 ].p.set(-dim, dimLess,  0 );   
  24.         pVertices[ 5 ].p.set(-dimLess, dimLess,  0 );   
  25.         pVertices[ 6 ].p.set(dimLess, dimLess,  0 );   
  26.         pVertices[ 7 ].p.set(dim, -dimLess,  0 );   
  27.         pVertices[ 8 ].p.set(-dim, -dimLess,  0 );   
  28.         pVertices[ 9 ].p.set(-dimLess, -dimLess,  0 );   
  29.         pVertices[ 10 ].p.set(dimLess, -dimLess,  0 );   
  30.         pVertices[ 11 ].p.set(dim, -dimLess,  0 );   
  31.         pVertices[ 12 ].p.set(-dim, -dim,  0 );   
  32.         pVertices[ 13 ].p.set(-dimLess, -dim,  0 );   
  33.         pVertices[ 14 ].p.set(dimLess, -dim,  0 );   
  34.         pVertices[ 15 ].p.set(dim, -dim,  0 );   
  35.            
  36.          //设置所有顶点的颜色和法线   
  37.          for  ( int  i =  0 ; i <  16 ; i++) {   
  38.              //设置法线   
  39.             pVertices[i].n.set(normal);   
  40.              //设置顶点色   
  41.             pVertices[i].c.set( 1 .0f,  1 .0f,  1 .0f,  0 .0f);   
  42.              //设置纹理坐标   
  43.             pVertices[i].t.x = pVertices[i].p.x +  0 .5f;   
  44.             pVertices[i].t.y = pVertices[i].p.y +  0 .5f;   
  45.              //设置顶点位置   
  46.             pVertices[i].p.x = pVertices[i].p.x * width;   
  47.             pVertices[i].p.y = pVertices[i].p.y * height;   
  48.         }   
  49.          //对于中心区域的4个顶点,设置为完全不透明,即alpha = 1.0   
  50.         pVertices[ 5 ].c.w =  1 .0f;   
  51.         pVertices[ 6 ].c.w =  1 .0f;   
  52.         pVertices[ 9 ].c.w =  1 .0f;   
  53.         pVertices[ 10 ].c.w =  1 .0f;   
  54.   
  55.          //创建镜像倒影   
  56.          for  ( int  row =  0 ; row <  4 ; ++row) {   
  57.              //根据镜像的高度调整顶点色明暗   
  58.              float  dark =  1  - ((pVertices[row *  4 ].p.y / height) +  0 .5f);   
  59.             dark -=  0 .5f;   
  60.   
  61.              for  ( int  col =  0 ; col <  4 ; col++) {   
  62.                  int  offset = row *  4  + col;   
  63.                  //将倒影封面的顶点垂直翻转   
  64.                 pVertices[offset +  16 ].set(pVertices[offset]);   
  65.                 pVertices[offset +  16 ].p.y = -pVertices[offset +  16 ].p.y;   
  66.                 pVertices[offset +  16 ].p.y -= height + heightFromMirror;   
  67.                    
  68.                  //设置倒影封面几何体的顶点颜色   
  69.                 pVertices[offset +  16 ].c.x = dark;   
  70.                 pVertices[offset +  16 ].c.y = dark;   
  71.                 pVertices[offset +  16 ].c.z = dark;   
  72.             }   
  73.     }  
public void createCover() { //封面有16个顶点,这里还包括它自身 的倒影,所以一共要创建32个 Vertex[] pVertices = new Vertex[16 * 2]; // 初始化操作 for (int i = 0; i < pVertices.length; i++) { pVertices[i] = new Vertex(); } // 设置封面初始化参数,宽度高度均为6.0f, 封面距离倒影的距离是0 // 如果需要显示宽屏的图片,可以在这里修改图片比例 float width = 6.0f, height = 6.0f, heightFromMirror = 0.0f; float dim = 0.5f; // as uv float mfBorderFraction = 0.05f;//过渡边缘宽度系数,如果设置为0就表示没有边缘过渡 float dimLess = 0.5f - (0.5f * mfBorderFraction); //设置顶点的法线,这里由于没有启用光照,因此并未特别设置 Vector3f normal = new Vector3f(0.0f, 1.0f, 0.0f); //设置顶点的位置 pVertices[0].p.set(-dim, dim, 0); pVertices[1].p.set(-dimLess, dim, 0); pVertices[2].p.set(dimLess, dim, 0); pVertices[3].p.set(dim, dim, 0); pVertices[4].p.set(-dim, dimLess, 0); pVertices[5].p.set(-dimLess, dimLess, 0); pVertices[6].p.set(dimLess, dimLess, 0); pVertices[7].p.set(dim, -dimLess, 0); pVertices[8].p.set(-dim, -dimLess, 0); pVertices[9].p.set(-dimLess, -dimLess, 0); pVertices[10].p.set(dimLess, -dimLess, 0); pVertices[11].p.set(dim, -dimLess, 0); pVertices[12].p.set(-dim, -dim, 0); pVertices[13].p.set(-dimLess, -dim, 0); pVertices[14].p.set(dimLess, -dim, 0); pVertices[15].p.set(dim, -dim, 0); //设置所有顶点的颜色和法线 for (int i = 0; i < 16; i++) { //设置法线 pVertices[i].n.set(normal); //设置顶点色 pVertices[i].c.set(1.0f, 1.0f, 1.0f, 0.0f); //设置纹理坐标 pVertices[i].t.x = pVertices[i].p.x + 0.5f; pVertices[i].t.y = pVertices[i].p.y + 0.5f; //设置顶点位置 pVertices[i].p.x = pVertices[i].p.x * width; pVertices[i].p.y = pVertices[i].p.y * height; } //对于中心区域的4个顶点,设置为完全不透明,即alpha = 1.0 pVertices[5].c.w = 1.0f; pVertices[6].c.w = 1.0f; pVertices[9].c.w = 1.0f; pVertices[10].c.w = 1.0f; //创建镜像倒影 for (int row = 0; row < 4; ++row) { //根据镜像的高度调整顶点色明暗 float dark = 1 - ((pVertices[row * 4].p.y / height) + 0.5f); dark -= 0.5f; for (int col = 0; col < 4; col++) { int offset = row * 4 + col; //将倒影封面的顶点垂直翻转 pVertices[offset + 16].set(pVertices[offset]); pVertices[offset + 16].p.y = -pVertices[offset + 16].p.y; pVertices[offset + 16].p.y -= height + heightFromMirror; //设置倒影封面几何体的顶点颜色 pVertices[offset + 16].c.x = dark; pVertices[offset + 16].c.y = dark; pVertices[offset + 16].c.z = dark; } }

渲染封面几何体
        为了更好的向大家演示边缘过渡的概念,我们特意把过渡系数调大(即在CoverRenderable类中的createCover()函数中设置mfBorderFraction,这里设置为0.25),最终效果如图6所示:

        大家可以很明显的看到每个封面都具有很宽的过渡边缘。由于封面渲染几何体被我们分成了两部分,一部分是完全不透明的中心区域,而另一部分是带透明信息的边缘过渡区域,因此在提交渲染时,我们需要渲染两次。第一次是禁用混合,只渲染不透明的部分;第二次是启用混合(GL_BLEND),并将混合方式设置为glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA),只渲染边缘过渡部分。图7和图8是分别独立渲染的截图:

 

 

         这样,两次渲染叠加之后,就会最终得到如图6的效果。

        在渲染过程中,由于第二次渲染启用了混合操作(GL_BLEND),因此要特别注意渲染的次序问题。对于需要混合的对象,必须要采取从后向前的渲染次序,也就是大家熟悉的画家算法,首先绘制离相机距离远的,最后再绘制离相机距离近的。如果次序错了,就会得到如图9所示的效果:

 

        大家可以看到,由于绘制次序的错误,本来封面的边缘过渡部分,需要和后面的封面像素相融合,但这里由于距离相机近的首先绘制,导致边缘混合时发现更远的位置上并没有填充应有像素,因此只能和黑色的背景色相混合,导致结果错误。因此,在启用混合操作时,渲染的次序尤其重要。

       对于同屏显示的众多封面,由于它们所最终渲染的对象实际上是一样的,不用的仅仅是各自的模型变换矩阵,因此只需要创建一份共享的封面几何体渲染对象的实例,在渲染不同位置的封面时,计算好相关的位置以及偏转角度,并设置好相关的封面纹理,最后将渲染数据统一提交即可。

输入事件响应
       本例中为了支持用户通过按键或者触屏操作进行左右滑动翻页,重载了GLSurfaceView中的onTouchEvent()以及onKeyDown()。同时为了确保输入响应操作能放在渲染线程中执行,需要调用queueEvent(new Runnable())将左右翻页的操作附加到渲染线程任务队列中。对于触屏操作,我们设置了一个最小响应距离,只有当用户拖动距离超过这个最小距离时,才会触发相应翻页操作。相关代码如下:

  1. /**  
  2.  * 响应触屏事件  
  3.  */   
  4. @Override   
  5. public   boolean  onTouchEvent(MotionEvent e) {   
  6.      float  x = e.getX();   
  7.      switch  (e.getAction()) {   
  8.      case  MotionEvent.ACTION_MOVE:   
  9.          float  dx = x - mPreviousX;   
  10.   
  11.          if (dx > MIN_DIS) {   
  12.             queueEvent( new  Runnable() {   
  13.                   // This method will be called on the rendering   
  14.                   // thread:   
  15.                   public   void  run() {   
  16.                      mRenderer.slideLeft();   
  17.                  }});   
  18.         }  else   if (dx < -MIN_DIS) {   
  19.             queueEvent( new  Runnable() {   
  20.                   // This method will be called on the rendering   
  21.                   // thread:   
  22.                   public   void  run() {   
  23.                      mRenderer.slideRight();   
  24.                  }});   
  25.         }  else  {   
  26.              //do nothing   
  27.         }   
  28.     }   
  29.     mPreviousX = x;   
  30.      return   true ;   
  31. }   
  32.   
  33. /**  
  34.  * 响应按键事件  
  35.  */   
  36. @Override   
  37. public   boolean  onKeyDown( int  keyCode, KeyEvent event) {   
  38.         if  (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {   
  39.            queueEvent( new  Runnable() {   
  40.                 // This method will be called on the rendering   
  41.                 // thread:   
  42.                 public   void  run() {   
  43.                 mRenderer.slideLeft();   
  44.                }});   
  45.             return   true ;   
  46.        }  else   if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {   
  47.         queueEvent( new  Runnable() {   
  48.                 // This method will be called on the rendering   
  49.                 // thread:   
  50.                 public   void  run() {   
  51.                 mRenderer.slideRight();   
  52.                }});   
  53.             return   true ;   
  54.        }   
  55.         return   super .onKeyDown(keyCode, event);   
 /** * 响应触屏事件 */ @Override public boolean onTouchEvent(MotionEvent e) { float x = e.getX(); switch (e.getAction()) { case MotionEvent.ACTION_MOVE: float dx = x - mPreviousX; if(dx > MIN_DIS) { queueEvent(new Runnable() { // This method will be called on the rendering // thread: public void run() { mRenderer.slideLeft(); }}); } else if(dx < -MIN_DIS) { queueEvent(new Runnable() { // This method will be called on the rendering // thread: public void run() { mRenderer.slideRight(); }}); } else { //do nothing } } mPreviousX = x; return true; } /** * 响应按键事件 */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { queueEvent(new Runnable() { // This method will be called on the rendering // thread: public void run() { mRenderer.slideLeft(); }}); return true; } else if(keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { queueEvent(new Runnable() { // This method will be called on the rendering // thread: public void run() { mRenderer.slideRight(); }}); return true; } return super.onKeyDown(keyCode, event); }

总结
      在本例中,我们通过开发一个3D封面效果演示程序,向大家进一步介绍了OPhone平台中强大的3D特性,包括纹理与顶点色的融合,以及Alpha混合等。借助于OPhone平台,开发者可以开发出更加炫酷的效果,让自己的程序具有更加强大的表现力。

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值