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

2.4 体积雾

第一卷第10章中介绍过简单的雾特效,通过其可以模拟很多现实世界中与雾、烟等相关的场景。但是简单的雾特效也有一定的局限性,如在实现山中烟雾缭绕的效果时就比较假。这是由于简单的雾特效没有考虑到变化的情况,只是采用简单的与距离相关的公式计算雾浓度因子。

而现实世界中的山中雾气往往是随风变化,并不是在所有的位置都遵循完全一致的雾浓度因子计算公式。本节将介绍一种能更好地模拟山岚烟云效果的雾特效技术——体积雾,通过其可以开发出非常真实的山中烟雾缭绕的效果。

2.4.1 基本原理

介绍具体的案例之前,首先需要了解一下本节案例实现体积雾的基本原理。体积雾实现的关键点在于计算出每个待绘制片元的雾浓度因子,然后根据雾浓度因子、雾的颜色及片元本身采样的纹理颜色计算出片元的最终颜色。

读者可能会有一个疑问:简单的雾特效采用的不也是这样的策略吗?确实如此,基本的大思路很类似,但体积雾雾浓度因子的计算模型不像简单的雾特效那样是一个简单呆板的公式,具体的计算策略如图2-16所示。

▲图2-16 体积雾计算模型原理

从图2-16中可以看出,体积雾具体的计算策略如下(此计算由片元着色器完成)。

● 首先通过当前待处理片元的位置与摄像机的位置确定一根射线,并求出射线与雾平面的交点位置。

● 若上述交点在雾平面以下,则求出交点到待处理片元位置的距离。

● 根据此距离的大小求出雾浓度因子,距离越大雾越浓。

提示

为了进一步增加真实感,实际案例中的雾平面并不是一个完全的平面,而是加入了正弦函数的高度扰动使得雾平面看起来有波动效果,如图2-16中右侧所示。

2.4.2 体积雾开发步骤

了解了实现体积雾的基本原理后,接着可以了解一下本节案例的运行效果,具体运行情况如图2-17所示。

▲图2-17 体积雾运行效果图

从图2-17中可以看出,山间飘荡着黄色的雾气,似有似无,效果非常真实。但由于本书正文中的插图采用灰度印刷,而且图是静态的,因此,强烈建议读者使用设备运行观察一下,那样才可以看到非常真实的效果。

了解了本节案例的运行效果后,下面就可以介绍案例的具体开发了。由于本案例中的大部分代码与第一卷中介绍过程纹理地形时给出的案例非常类似,因此,这里仅给出本案例中最有代表性的部分,具体内容如下所列。

(1)观察过本节案例的运行效果后就会发现,场景中的山间雾气并不是静止的,而是沿着起始角连续变化的正弦曲线飘动的。为了实现雾气飘动的效果,Mountion类中的drawSelf方法将连续变化的起始角在绘制每帧画面前传入渲染管线。此起始角与用于扰动雾平面高度的正弦曲线对应,相关的代码如下。

代码位置:见随书光盘中源代码/第2章/Sample2_11/src/com/bn/Sample2_11目录下的Mountion.java。

      1           //将体积雾的雾平面高度传入渲染管线
      2           GLES20.glUniform1f(slabYHandle, TJ_GOG_SLAB_Y);
      3           //将体积雾扰动起始角传入渲染管线
      4           GLES20.glUniform1f(startAngleHandle,(float)Math.toRadians(startAngle));
      5           //修改扰动角的值,每次加3,取值范围永远在0~360的范围内
      6           startAngle=(startAngle+3f)%360.0f;

提示

上述代码的功能非常简单,首先将体积雾所需雾平面的高度传入渲染管线,接着将用于扰动雾平面高度的正弦曲线所需起始角传入渲染管线。最后增加起始角的值,并通过取模的方式将起始角限制在0~360的范围内。

(2)上一小节通过图2-16给出了体积雾计算模型的基本原理,也提到过此计算是由片元着色器完成的,下面就给出实现此计算的片元着色器的详细代码,具体内容如下。

代码位置:见随书光盘中源代码/第2章/Sample2_11/src/com/bn/Sample2_11目录下的frag.sh。

      1  precision mediump float;                        //给出默认的浮点精度
      2  varying vec2 vTextureCoord;                     //接收从顶点着色器传过来的纹理坐标
      3  varying float currY;                            //接收从顶点着色器传过来的y坐标
      4  varying vec4 pLocation;                         //接收从顶点着色器传过来的顶点坐标
      5  uniform float slabY;                            //体积雾对应雾平面的高度
      6  uniform float startAngle;                       //扰动起始角
      7  uniform vec3 uCamaraLocation;                   //摄像机位置
      8  uniform sampler2D sTextureGrass;               //纹理内容数据(草皮)
      9  uniform sampler2D sTextureRock;                 //纹理内容数据(岩石)
      10 uniform float landStartY;                      //过程纹理起始y坐标
      11 uniform float landYSpan;                       //过程纹理跨度
      12 float tjFogCal(vec4 pLocation){                //计算体积雾浓度因子的方法
      13   float xAngle=pLocation.x/16.0*3.1415926;    //计算出顶点x坐标折算出的角度
      14   float zAngle=pLocation.z/20.0*3.1415926;    //计算出顶点z坐标折算出的角度
      15   float slabYFactor=sin(xAngle+zAngle+startAngle);//联合起始角计算出角度和的正弦值
      16   //求从摄像机到待处理片元的射线参数方程Pc+(Pp-Pc)t与雾平面交点的t值
      17   float t=(slabY+slabYFactor-uCamaraLocation.y)/(pLocation.y-uCamaraLocation.y);
      18   //有效的t的范围应该在0~1的范围内,若不在范围内表示待处理片元不在雾平面以下
      19   if(t>0.0&&t<1.0){                                 //若在有效范围内则
      20         //求出射线与雾平面的交点坐标
      21         float xJD=uCamaraLocation.x+(pLocation.x-uCamaraLocation.x)*t;
      22         float zJD=uCamaraLocation.z+(pLocation.z-uCamaraLocation.z)*t;
      23         vec3 locationJD=vec3(xJD,slabY,zJD);
      24         float L=distance(locationJD,pLocation.xyz);//求出交点到待处理片元位置的距离
      25         float L0=10.0;
      26         return L0/(L+L0);                            //计算体积雾的雾浓度因子
      27      }else{
      28         return 1.0;                //若待处理片元不在雾平面以下,则此片元不受雾影响
      29   }}
      30 void main(){
      31      vec4 gColor=texture2D(sTextureGrass, vTextureCoord);   //从草皮纹理中采样出颜色
      32      vec4 rColor=texture2D(sTextureRock, vTextureCoord);    //从岩石纹理中采样出颜色
      33      vec4 finalColor;                                         //片元最终颜色
      34      if(currY<landStartY){
      35         finalColor=gColor;         //当片元y坐标小于过程纹理起始y坐标时采用草皮纹理
      36      }else if(currY>landStartY+landYSpan){
      37         finalColor=rColor;         //当片元y坐标大于过程纹理起始y坐标加跨度时采用岩石纹理
      38      }else{                        //当片元y坐标在过程纹理范围内时将草皮和岩石混合
      39         float currYRatio=(currY-landStartY)/landYSpan;  //计算岩石纹理所占的百分比
      40         //将岩石、草皮纹理颜色按比例混合
      41         finalColor= currYRatio*rColor+(1.0- currYRatio)*gColor;
      42      }
      43         float fogFactor=tjFogCal(pLocation);            // 计算雾浓度因子
      44         //根据雾浓度因子、雾的颜色及片元本身采样的纹理颜色计算出片元的最终颜色
      45         gl_FragColor=fogFactor*finalColor+
      46       (1.0-fogFactor)*vec4(0.9765,0.7490,0.0549,0.0);
      47 }

● 第12-29行为本案例中最有代表性的,根据传入着色器的参数计算体积雾浓度因子的tjFogCal方法。此方法首先根据起始角和对应片元位置折算出的角度计算出一个正弦值,然后将此正弦值加上雾平面的高度作为扰动后的雾平面高度。然后计算出从摄像机到待处理片元的射线对应的参数方程(摄像机位置+(待处理片元位置-摄像机位置)×t)与扰动后雾平面交点处的参数值(t值)。若t值在0~1的范围内(表示待处理片元在雾平面以下),则根据待处理片元的位置到交点的距离计算出雾浓度因子的大小。

● 第30-47行为片元着色器的main方法,其中首先执行了过程纹理计算,根据片元高度计算出了待处理片元的纹理采样颜色值。然后计算出体积雾浓度因子,最后根据雾浓度因子、雾的颜色及片元本身的纹理采样颜色计算出片元的最终颜色值。

到这里为止,体积雾技术就介绍完了,经过上面的介绍读者可能已经发现体积雾并不是实际存在的3D模型,只是在应该被雾覆盖的片元上通过某种计算模型的计算混合了雾的颜色,最后造成了有雾覆盖的效果。同时体积雾的实际计算模型有很多,本节只是给出了比较简单的一种。读者可以根据具体的需要以及本节介绍的体积雾的思想,开发出效果更加真实和酷炫的体积雾计算模型。