OpenGL ES 2.0游戏开发(下卷)
上QQ阅读APP看书,第一时间看更新

第4章 阴影及高级光照

本章将为读者介绍OpenGL ES 2.0中实现阴影的几种常见技术,主要包括平面阴影、阴影映射、阴影贴图、光线跟踪等几个方面。同时,本章还介绍了几种常用的技术,这就是投影贴图、聚光灯高级光源,以及高真实感水面绘制。掌握了这些技术以后,读者将能开发出更加真实、更加吸引用户的酷炫场景。

4.1 投影贴图

现实世界中很多场合都会有如下情况:在光源前面放一张透明胶片,胶片上面有半透明的图案,经光源照射后透明胶片上的图案将投射到被光源照射的物体上。本节将通过一个案例Sample4_1向读者介绍如何基于OpenGL ES 2.0来开发这样的场景。

4.1.1 投影案例效果与基本原理

介绍本案例的具体开发步骤之前,首先需要了解一下案例的运行效果与基本原理,其运行效果如图4-1所示。

▲图4-1 投影贴图案例运行效果图

▲图4-2 投影用纹理

从运行效果图4-1中可以看出,光源位于场景的一侧(靠近圆环与茶壶,距离立方体与圆球较远),从光源处将纹理图(如图4-2所示)投射到整个场景中。而且被光源投射的纹理图还编程实现了旋转的效果,故场景中的投影是不断变化的。

提示

运行本案例时当手指在屏幕上水平滑动时摄像机会绕场景转动,当手指在屏幕上垂直滑动时摄像机会随手指的移动而升降。

实现上述场景的基本技术是纹理映射(这种特殊的纹理映射也被称为投影贴图),只不过与前面章节介绍的普通纹理映射不同,这里的纹理坐标不是预先在初始化时分配给顶点的,而是在着色器中根据传入的相关参数实时计算出来的,基本思想如图4-3所示。

从图4-3中可以看出,此案例中最重要的工作就是在着色器中根据光源位置、透明胶片纹理图的位置、尺寸及片元的位置计算出片元对应的纹理坐标。这项工作如果直接用空间解析几何计算虽然是可以完成的,但会非常烦琐。

▲图4-3 投影贴图的基本原理

其实转变一下思维方式就很容易实现,可以在光源处虚拟一个摄像机,对应于此虚拟摄像机的投影参数进行如下设置。

● 将left与right各设置为0.5,总和为1,代表透明胶片纹理图的宽度纹理坐标跨度。

● 将top与bottom也各自设置为0.5,总和为1,代表透明胶片纹理图的高度纹理坐标跨度。

● 将near设置为透明胶片距光源的距离,far设置为不小于光源到需要照射的最远物体的距离。

然后将摄像机观察矩阵及投影矩阵的组合矩阵传入着色器,在着色器中将片元位置与此矩阵进行运算即可计算出此片元位置投影到光源处虚拟摄像机近平面上的位置。由于前面将近平面的尺寸设置为1.0×1.0了,所以此近平面上的位置可以非常方便地换算成合法的纹理坐标,如图4-4所示。

▲图4-4 纹理坐标换算

从图4-4中可以看出换算公式为。

s=x+0.5

t=y+0.5

计算出纹理坐标后就非常简单了,只需要将纹理坐标传递给纹理采样函数进行纹理采样即可得到片元的颜色,投影贴图也就实现了。

4.1.2 开发步骤

了解了案例的运行效果及基本原理后,就可以进行案例的开发了,具体步骤如下。

(1)首先用3dsMax生成5个基本物体(平面、圆环、茶壶、立方体、圆球),并导出成obj文件放入项目的assets目录待用。

(2)开发出搭建场景的基本代码,包括:加载物体、摆放物体、计算光照等。这些代码与前面章节的许多案例基本套路完全一致,因此这里不再赘述。

(3)由于需要将光源处虚拟摄像机的投影与观察组合矩阵传入着色器,因此需要对MatrixState类进行升级,增加获取投影与观察组合矩阵的getViewProjMatrix方法,其代码如下。

代码位置:见随书光盘中源代码/第4章/Sample4_1/com/bn/Sample4_1目录下的MatrixState.java。

      1     public static float[] getViewProjMatrix(){
      2      float[] mMVPMatrix=new float[16];       //创建结果矩阵数组
      3      Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);
                                                        //将投影及观察矩阵相乘得到组合矩阵
      4      return mMVPMatrix;                       //返回组合矩阵
      5     }

(4)在MySurfaceView中绘制场景时需要比原来增加获取投影与观察组合矩阵的代码,具体情况如下。

代码位置:见随书光盘中源代码/第4章/Sample4_1/com/bn/Sample4_1目录下的MySurfaceView.java。

      1    public void onDrawFrame(GL10 gl){
      2    MatrixState.setCamera(lx,ly,lz,10f,0f,10f,ux,uy,uz);
                                                                //产生位于光源处虚拟摄像机的观察矩阵
      3    MatrixState.setProjectFrustum(-0.5f, 0.5f, -0.5f, 0.5f, 1f, 400);
                                                                //产生位于光源处虚拟摄像机的投影矩阵
      4    mMVPMatrixGY=MatrixState.getViewProjMatrix();    //获取虚拟摄像机的观察、投影组合矩阵
      5    GLES20.glClear(                                 //清除深度缓冲与颜色缓冲
      6     GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
      7    MatrixState.setCamera(cx,cy,cz,0f,0f,0f,0f,1f,0f); //生成实际摄像机观察矩阵
      8    MatrixState.setProjectFrustum(-ratio, ratio, -1.0f, 1.0f, 2, 1000);
                                                                //生成实际摄像机投影矩阵
      9    MatrixState.setLightLocation(lx, ly, lz);        //设置光源位置
      10   ……//此处省略绘制场景中各个物体的代码,需要的读者请参考随书光盘
      11 }

(5)接着还需要在MySurfaceView类构造器中增加定时修改光源处虚拟摄像机对应up向量的代码,具体内容如下。

代码位置:见随书光盘中源代码/第4章/Sample4_1/com/bn/Sample4_1目录下的MySurfaceView.java。

      1    new Thread(){
      2      public void run(){
      3        while(true){
      4           lAngle += 0.5;                                   //改变up向量绕 Y轴的旋转角度
      5           ux=(float)Math.sin(Math.toRadians(lAngle))*lR;//根据角度计算当前up向量的X分量
      6           uz=(float)Math.cos(Math.toRadians(lAngle))*lR;
                                                                //根据角度计算当前up向量的Z分量
      7           try { Thread.sleep(20); } catch(InterruptedException e){e.printStack
                  Trace();}                                   /线程休眠
      8    } } }.start();

说明

上述代码中的线程定时(每20ms)修改光源处虚拟摄像机对应up向量绕Y轴旋转的角度,以在程序中实现被投影的纹理图不断旋转的效果。

(6)绘制加载物体的类也要相对于前面章节的版本进行升级,增加传递位于光源处虚拟摄像机投影、观察组合矩阵进渲染管线的相关代码。这部分代码与传递其他矩阵的代码套路相同,这里不再赘述,需要的读者请参考随书光盘。

(7)本案例中的顶点着色器代码与前面光照部分的基本相同,仅仅增加了将顶点位置传递给片元着色器的功能,这里也不再赘述,需要的读者请参考随书光盘。

(8)本案例中的片元着色器是实现投影贴图的关键所在,其代码如下。

代码位置:见随书光盘中源代码/第4章/Sample4_1/assets目录下的frag.sh。

      1    precision mediump float;             //设置默认精度
      2    uniform sampler2D sTexture;          //纹理内容数据
      3    varying vec4 ambient;                //接收从顶点着色器传递过来的环境光参数
      4    varying vec4 diffuse;                //接收从顶点着色器传递过来的散射光参数
      5    varying vec4 specular;               //接收从顶点着色器传递过来的镜面光参数
      6    varying vec4 vPosition;              //接收从顶点着色器传递过来的片元位置
      7    uniform highp mat4 uMVPMatrixGY;     //光源位置处虚拟摄像机观察及投影组合矩阵
      8    void main(){
      9             //将片元的位置投影到光源处虚拟摄像机的近平面上
      10      vec4 gytyPosition=uMVPMatrixGY * vec4(vPosition.xyz,1);
      11      gytyPosition=gytyPosition/gytyPosition.w; //进行透视除法
      12      float s=gytyPosition.s+0.5;               //将投影后的坐标换算为纹理坐标
      13      float t=gytyPosition.t+0.5;
      14      vec4 finalColor=vec4(0.8,0.8,0.8,1.0);    //物体本身的颜色
      15      if(s>=0.0&&s<=1.0&&t>=0.0&&t<=1.0){       //若纹理坐标在合法范围内则考虑投影贴图
      16              vec4 projColor=texture2D(sTexture, vec2(s,t)); //对投影纹理图进行采样
      17              vec4 specularTemp=projColor*specular;      //计算投影贴图对镜面光的影响
      18              vec4 diffuseTemp=projColor*diffuse;        //计算投影贴图对散射光的影响
      19              //计算最终片元颜色
      20              gl_FragColor = finalColor*ambient+finalColor*specularTemp+finalColor
                            *diffuseTemp;
      21      } else {//计算最终片元颜色
      22           gl_FragColor = finalColor*ambient+finalColor*specular+finalColor*diffuse;
      23      }
      24   }

● 第10-13行完成了最核心的工作,首先将此片元投影到位于光源位置的摄像机对应的近平面上,然后将投影坐标换算为纹理坐标。

● 第16-23行首先判断换算出的纹理坐标是否在合理的范围内(0.0~1.0),若在则对投影纹理图进行采样,并将采样结果与散射光、镜面光分量进行加权计算,最终将3个光通道与物体颜色加权计算得到片元颜色。若纹理坐标不在合理范围内,则直接进行3个光通道与物体颜色的加权计算。

到这里为止投影贴图技术就介绍完了,在以后的具体项目开发中读者若能做到灵活运用,将可以开发出更加逼真的场景。