3.6 真实感很强的地形
《Open GLE开发实战详解》中曾经介绍过灰度图地形技术,通过其可以生成较为真实的山地地形。但第一卷中给出的具体实现还有两个明显的缺憾:第一就是山地没有光照效果,层次感、立体感不够好;第二就是山地的各个方向视觉效果没有差异(现实世界中,向阳面和背阴面是不一样的)。因此,本节将对第一卷中的灰度图地形进行升级,给出效果更好的解决方案。
3.6.1 真实感地形的基本原理
给出具体的案例之前,首先要介绍一下本节案例相对于《Open GLE开发实战详解》中灰度图地形案例具体的升级细节,详细内容如下所列。
● 通过灰度图生成地形对应的顶点时,不单计算出每个顶点的位置,还计算出每个顶点的法向量。这样就可以为整个地形加上光照效果,提升场景的真实感。
● 根据山地片元处于向阳面还是背阴面以及海拔来决定给片元应用什么样的纹理。具体策略为:高于指定海拔的无论位于向阳面还是背阴面一律采用雪山纹理;低于指定海拔的,若位于向阳面则采用草皮纹理,反之则采用石头纹理。
可以想象出,采用上述策略对灰度图地形进行升级后,真实感会增加很多。现实世界中的山地很多就是在海拔超过一定高度(雪线)后是终年积雪,低于雪线的位置向阳面是生机盎然的绿色,背阴面是长着青苔的岩石。
3.6.2 真实感地形的开发步骤
了解了本节案例相对于第一卷中灰度图地形案例的升级细节以后,就可以进行案例的开发了。开发之前有必要首先了解一下本节案例的运行效果,如图3-15所示。
▲图3-15 高真实感地形背光面(左图)和迎光面(右图)
从图3-15中可以看出,增加了光照以及向阳面与背阴面应用不同纹理后,场景真实感相比第一卷中的对应案例有了很大的提升。同时,由于正文中的插图采用灰度印刷,可能看不清楚,建议读者采用真机设备运行观察体会。
了解了本节案例的运行效果后,下面就可以进行案例的开发了。由于本案例实际是对第一卷中灰度图地形案例的升级,故有很多代码与升级前的案例相同。因此,这里仅给出本案例中有代表性的部分,具体内容如下。
(1)首先给出的是在计算顶点坐标时一同计算顶点法向量的相关代码,具体内容如下。
代码位置:见随书光盘中源代码/第3章/Sample3_6/com/bn/Sample3_6目录下的MySurfaceView.java。
1 public static float[][][] caleNormal(float yArray[][]){//入口参数为从灰度图加载的高 度数组 2 //存放山地中顶点位置的三维数组 3 float [][][] vertices=new float [yArray.length][yArray[0].length][3]; 4 //存放山地中顶点法向量的三维数组 5 float [][][] normols=new float [yArray.length][yArray[0].length][3];
6 for(int i=0;i<yArray.length;i++){ //对高度数组遍历,计算顶点位置坐标 7 for(int j=0;j<yArray[0].length;j++){ 8 float zsx=-UNIT_SIZE*yArray.length/2+i*UNIT_SIZE; 9 float zsz=-UNIT_SIZE*yArray[0].length/2+j*UNIT_SIZE; 10 vertices[i][j][0]=zsx; //顶点的x坐标 11 vertices[i][j][1]=yArray[i][j]; //顶点的y坐标 12 vertices[i][j][2]=zsz; //顶点的z坐标 13 }} 14 //用于存放顶点法向量的HashMap,其键为顶点的索引,值为对应顶点的法向量集合 15 HashMap<Integer,HashSet<Normal>>hmn=new HashMap<Integer,HashSet<Normal>>(); 16 int rows = yArray.length-1; //地形网格的行数 17 int cols = yArray[0].length-1; //地形网格的列数 18 for(int i=0;i<rows;i++){ //对地形网格进行遍历 19 for(int j=0;j<cols;j++){ 20 int []index = new int[4]; //创建用于存放当前网格4个顶点索引的数组 21 index[0]=i*(cols+1)+j; //网格中0号点的索引 0------1 22 index[1]=index[0]+1; //网格中1号点的索引 | / | 23 index[2]=index[0]+cols+1; //网格中2号点的索引 | / | 24 index[3]=index[1]+cols+1; //网格中3号点的索引 2------3 25 //计算当前地形网格左上三角形面的法向量 26 float vxa=vertices[i+1][j][0]-vertices[i][j][0]; 27 float vya=vertices[i+1][j][1]-vertices[i][j][1]; 28 float vza=vertices[i+1][j][2]-vertices[i][j][2]; 29 float vxb=vertices[i][j+1][0]-vertices[i][j][0]; 30 float vyb=vertices[i][j+1][1]-vertices[i][j][1]; 31 float vzb=vertices[i][j+1][2]-vertices[i][j][2]; 32 float[] vNormal1=Normal.vectorNormal(Normal.getCrossProduct 33 (vxa,vya,vza,vxb,vyb,vzb)); 34 for(int k=0;k<3;k++){//将计算出的法向量加入各个顶点对应的法向量集合中 35 HashSet<Normal> hsn=hmn.get(index[k]); 36 if(hsn==null){ //若集合不存在则创建 37 hsn=new HashSet<Normal>();} 38 hsn.add(new Normal(vNormal1[0],vNormal1[1],vNormal1[2])); 39 hmn.put(index[k],hsn);//将集合放进HsahMap中 40 } 41 //计算当前地形网格右下三角形面的法向量 42 vxa=vertices[i+1][j][0]-vertices[i+1][j+1][0]; 43 vya=vertices[i+1][j][1]-vertices[i+1][j+1][1]; 44 vza=vertices[i+1][j][2]-vertices[i+1][j+1][2]; 45 vxb=vertices[i][j+1][0]-vertices[i+1][j+1][0]; 46 vyb=vertices[i][j+1][1]-vertices[i+1][j+1][1]; 47 vzb=vertices[i][j+1][2]-vertices[i+1][j+1][2]; 48 float[] vNormal2=Normal.vectorNormal(Normal.getCrossProduct 49 (vxb,vyb,vzb,vxa,vya,vza)); 50 for(int k=1;k<4;k++){//将计算出的法向量加入各个顶点对应的法向量集合中 51 HashSet<Normal> hsn=hmn.get(index[k]); 52 if(hsn==null){ //若集合不存在则创建 53 hsn=new HashSet<Normal>(); 54 } 55 hsn.add(new Normal(vNormal2[0],vNormal2[1],vNormal2[2])); 56 hmn.put(index[k], hsn); //将集合放进HsahMap中 57 }}} 58 for(int i=0;i<yArray.length;i++){ //遍历顶点数组,计算每个顶点的平均法向量 59 for(int j=0;j<yArray[0].length;j++){ 60 int index=i*(cols+1)+j; 61 HashSet<Normal> hsn=hmn.get(index); 62 float[] tn=Normal.getAverage(hsn); //求出平均法向量 63 normols[i][j] = tn; //将计算出的平均法向量存放到法向量数组中 64 }} 65 return normols; //返回法向量数组 66 }
● 第6-13行功能为遍历从灰度图加载的地形顶点高度数组,从而计算出地形网格中每个顶点的坐标。计算出各个顶点的坐标后存入一个三维数组中,供后面计算每个顶点的法向量时使用。
● 第18-57行功能为按照行列遍历每个地形网格,遍历到一个地形网格后分左上和右下两个三角形分别计算出两个三角形面的法向量,然后将计算出的法向量存入到三角形每个顶点对应的法向量集合中。
● 第58-64行功能为遍历每个顶点,将顶点对应的法向量集合中的法向量求出平均值,作为此顶点的法向量存入结果数组中。
提示
第18-57行中计算三角形面的法向量时采用的是站在一个顶点上,求出此顶点到另外两个顶点的向量,然后将这两个向量求叉积(矢量积)的方法。这种方法计算非常简单,笔者很喜欢采用。
(2)介绍完了计算顶点法向量的方法后,下面需要介绍的就是完成根据不同情况对片元采用不同纹理图进行着色工作的片元着色器了,其具体代码如下。
代码位置:见随书光盘中源代码/第3章/Sample3_6/com/bn/Sample3_6目录下的frag.sh。
1 precision mediump float; //给出默认的浮点精度 2 varying vec2 vTextureCoord; //接收从顶点着色器传过来的纹理坐标 3 varying float currY; //接收从顶点着色器传过来的y坐标 4 varying vec3 vPosition; //接收从顶点着色器传过来的顶点位置 5 varying vec4 vAmbient; //接收从顶点着色器传过来的环境光分量 6 varying vec4 vDiffuse; //接收从顶点着色器传过来的散射光分量 7 varying vec4 vSpecular; //接收从顶点着色器传过来的镜面反射光分量 8 uniform sampler2D sTextureGrass; //纹理内容数据(草皮) 9 uniform sampler2D sTextureRock; //纹理内容数据(岩石) 10 uniform sampler2D sTextureIce; //纹理内容数据(积雪) 11 uniform float landStartY; //过程纹理起始y坐标 12 uniform float landYSpan; //过程纹理跨度 13 void main(){ 14 vec4 gColor=texture2D(sTextureGrass,vTextureCoord); //从草皮纹理中采样出颜色 15 vec4 rColor=texture2D(sTextureRock,vTextureCoord); //从岩石纹理中采样出颜色 16 vec4 iColor=texture2D(sTextureIce,vTextureCoord); //从积雪纹理中采样出颜色 17 vec4 finalColor; //最终颜色 18 if(vDiffuse.x>0.21){ //判断散射光分量是否大于0.21 19 finalColor=gColor; //把草地的颜色值赋给最终片元颜色值 20 }else if(vDiffuse.x<0.05){ //判断散射光分量是否小于0.05 21 finalColor=rColor; //把岩石的颜色值赋给最终片元颜色值 22 }else{ //若散射光分量在渐变范围内 23 //把岩石和草地的颜色混合值赋给最终片元颜色值 24 float t=(vDiffuse.x-0.05)/0.16; 25 finalColor=t*gColor+(1.0-t)*rColor; 26 } 27 if(currY<landStartY){ 28 }else if(currY>landStartY+landYSpan){ 29 finalColor=iColor; //当片元y坐标大于过程纹理起始y坐标加跨度时采用雪纹理 30 }else{ //当片元y坐标在渐变范围内 31 float currYRatio=(currY-landStartY)/landYSpan; //计算雪纹理所占的百分比 32 finalColor=currYRatio*iColor+ 33 (1.0-currYRatio)*finalColor; //将雪、草皮或雪、岩石纹理颜色按比例混合 34 } 35 gl_FragColor=finalColor*vAmbient+ //结合光照计算出此片元最终颜色值 36 finalColor*vDiffuse+finalColor*vSpecular; 37}
● 第14-16行为分别从草地、岩石、积雪纹理中采样出所需的片元颜色。
● 第18-26行功能为首先判断散射光分量是否大于0.21,如果大于,那么把草地的颜色值赋给最终颜色值。然后判断散射光分量是否小于0.05,如果小于那么把岩石的颜色值赋给最终颜色值。最后,如果散射光分量大于0.05并且小于0.21,那么把岩石和草地的颜色值按比例混合赋给最终颜色值。
● 第27-33行功能为根据顶点的高度计算出片元的最终颜色值。具体策略为高于指定值(andStartY+landYSpan)的部分直接采用积雪颜色,低于另外一个指定值(landStartY)的部分直接采用前面计算出的草皮、岩石或草皮岩石混合颜色。在两个值中间的将积雪颜色和前面计算出的颜色按比例混合得到片元最终的颜色值。
● 第35-36行功能为将计算出的片元颜色值结合环境光、散射光、镜面反射光3个光照通道的强度计算出最终的片元颜色并传递给渲染管线。
到这里为止,高真实感地形就介绍完了,在以后的具体项目开发中读者若能灵活运用,将可以开发出非常逼真的场景。