第3章 真实光学环境的模拟
本章将在前面知识的基础上进一步向读者介绍,如何通过OpenGL ES 2.0模拟现实世界的一些常见光学效果,如反射、折射等。掌握了这些技术以后,读者将能开发出更加真实、更加吸引用户的酷炫场景。
3.1 反射环境模拟
现实世界中很多场合都会有这样的情况,放置在环境中具有良好反射表面的物体会反射出周围环境的内容。例如,在环境中放置了一套表面抛光的银质餐具,餐具的光滑表面就会映射出周围环境的内容。本节将通过一个案例Sample3_1向读者介绍如何基于OpenGL ES 2.0来开发这样的效果。
3.1.1 案例效果与基本原理
介绍本案例的具体开发步骤之前首先需要了解一下案例的运行效果与基本原理,其运行效果如图3-1所示。
▲图3-1 反射环境纹理案例运行效果图
从运行效果图3-1中可以看出,本节案例在一个自然场景中放置了一把具有光滑反射表面的茶壶,茶壶表面反射出了周围场景中的内容。自然场景是用前面章节介绍过的天空盒技术实现的,光滑表面的茶壶反射周围的环境内容是通过立方图纹理技术实现的。
提示
本案例运行时,当手指在屏幕上水平滑动时摄像机会绕场景转动,当手指在屏幕上垂直滑动时摄像机会随手指的移动而升降。
立方图纹理是一种特殊的纹理映射技术,主要包括如下两个要点。
● 立方图纹理的单位是套,一套立方图纹理包括6幅尺寸相同的正方形纹理图。与构造天空盒的思路相同,这6幅图正好包含了周天360°全部的场景内容。
● 对立方图纹理进行采样时,需要给出的不再是S、T两个轴的纹理坐标,而是一个规格化的向量。此规格化向量代表采样的方向,用来确定在代表全周天360°的6幅图中的哪一幅的哪个位置进行采样,具体情况如图3-2所示。
从图3-2中可以看出,本案例中实现茶壶反射周围环境的内容有两项关键工作,具体如下所列。
● 根据摄像机位置及被观察点位置计算出观察方向(视线)向量,并参照被观察点的法向量计算出视线反射方向向量。
● 根据视线反射方向向量确定采样点,实施立方图纹理采样。这与现实世界中人眼观察光滑物体,其表面有反射是完全一致的。
提示
上述两项工作中的第一项需要自己编程在着色器中实现,第二项只需要拿着采样向量调用textureCube函数即可完成。
▲图3-2 立方图纹理采样技术实现反射效果的原理
3.1.2 反射环境开发步骤
了解了案例的运行效果及基本原理后,就可以进行案例的开发了,具体步骤如下。
(1)首先用3ds Max生成一个茶壶,并导出成obj文件放入项目的assets目录中待用。
(2)开发出搭建场景的基本代码,包括:天空盒、加载物体、摆放物体等。这些代码与前面章节的许多案例基本套路完全一致,因此这里不再赘述。
(3)由于本案例中茶壶采用的是立方图纹理,所以在MySurfaceView类中需要首先增加初始化立方图纹理的工具方法generateCubeMap,其代码如下。
代码位置:见随书光盘中源代码/第3章/Sample3_1/com/bn/Sample3_1目录下的MySurfaceView.java。
1 public int generateCubeMap(int[] resourceIds){ 2 int[] ids = new int[1]; //用于存储生成的纹理编号的数组 3 GLES20.glGenTextures(1, ids, 0); //生成一个新的纹理编号 4 int cubeMapTextureId = ids[0]; 5 GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, cubeMapTextureId);//绑定纹理 6 //设置纹理采样方式、拉伸方式 7 GLES20.glTexParameterf(GLES20.GL_TEXTURE_CUBE_MAP, 8 GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); 9 GLES20.glTexParameterf(GLES20.GL_TEXTURE_CUBE_MAP, 10 GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); 11 GLES20.glTexParameterf(GLES20.GL_TEXTURE_CUBE_MAP, 12 GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_REPEAT); 13 GLES20.glTexParameterf(GLES20.GL_TEXTURE_CUBE_MAP, 14 GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_REPEAT); 15 for(int face = 0; face < 6; face++){//循环加载立方图纹理各个面的图片 16 InputStream is = getResources().openRawResource(resourceIds[face]); 17 Bitmap bitmap; 18 try { bitmap = BitmapFactory.decodeStream(is); } finally { 19 try {is.close();} catch(IOException e){ 20 Log.e("CubeMap", "Could not decode texture for face " + Integer.toString (face));}} 21 GLUtils.texImage2D(//加载立方图纹理中的指定编号的面 22 GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0,bitmap, 0);
23 bitmap.recycle(); //释放图片所占内存 24 } 25 return cubeMapTextureId; //返回加载完成的立方图纹理编号 26 }
● 第1行方法的入口参数与以前加载纹理的方法不同,不再是一幅图片的资源id,而是一个资源id数组,其用来接收组成一套立方图纹理的6幅图片的资源id。
● 第7-14行设置了纹理的采样与拉伸方式,与前面的案例类似,但要注意的是所有设置方法的第一个参数不再是“GL_TEXTURE_2D”,而是“GL_TEXTURE_CUBE_MAP”。
● 第15-24行循环将立方图纹理中各个面的图片加载进内存,然后再加载进显存。
(4)开发完了加载立方图纹理的generateCubeMap方法后,就可以在MySurfaceView类的onSurfaceCreated方法中调用其进行立方图纹理的初始化了,代码片段如下。
代码位置:见随书光盘中源代码/第3章/Sample3_1/com/bn/Sample3_1目录下的MySurfaceView.java。
1 int[] cubeMapResourceIds = new int[]{ //组织图片资源id数组 2 R.raw.skycubemap_right, R.raw.skycubemap_left, R.raw.skycubemap_up_cube, 3 R.raw.skycubemap_down, R.raw.skycubemap_front, R.raw.skycubemap_back}; 4 textureIdCM=generateCubeMap(cubeMapResourceIds); //加载立方图纹理
提示
请读者注意图片资源id的顺序为“右、左、上、下、前、后”,如果顺序不对多幅纹理图可能会拼接不上。
(5)完成了上述工作后只要在负责绘制茶壶的LoadedObjectVertexNormalTexture类中的drawSelf方法中增加绑定立方图纹理的相关代码即可,具体内容如下。
代码位置:见随书光盘中源代码/第3章/Sample3_1/com/bn/Sample3_1目录下的LoadedObject VertexNormalTexture.java。
1 GLES20.glEnable(GLES20.GL_TEXTURE_CUBE_MAP); //启用立方图纹理 2 GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP,texId);//绑定立方图纹理
提示
LoadedObjectVertexNormalTexture类中的其他代码与前面案例中的基本没有区别,需要的读者请自行参考随书光盘中的源代码。
(6)完成了Java代码之后就可以开发着色器了。本案例中有两套着色器:一套是普通贴纹理的,用来绘制天空盒的各个面;另一套是负责茶壶立方图纹理实施的。第一套与原来案例中的完全一样,这里不再赘述。下面着重介绍用来绘制茶壶的、立方图纹理相关的着色器,首先是顶点着色器,其代码如下。
代码位置:见随书光盘中源代码/第3章/Sample3_1/assets目录下的vertex_tex_cube.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵 2 uniform mat4 uMMatrix; //变换矩阵 3 uniform vec3 uCamera; //摄像机位置 4 attribute vec3 aPosition; //顶点位置 5 attribute vec3 aNormal; //顶点法向量 6 varying vec3 vTextureCoord; //用于传递给片元着色器的立方图采样向量 7 void main(){ 8 gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制此顶点位置 9 //计算变换后的法向量并规格化 10 vec3 normalTarget=aPosition+aNormal; 11 vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz; 12 newNormal=normalize(newNormal); 13 //计算从观察点到摄像机的向量(视线向量) 14 vec3 eye=- normalize(uCamera-(uMMatrix*vec4(aPosition,1)).xyz); 15 vTextureCoord=reflect(eye,newNormal); //计算视线向量的反射向量并传递给片元着色器 16 }
说明
上述顶点着色器与实现普通纹理映射功能的顶点着色器最大的区别是,传递给片元着色器的纹理坐标不是从管线接收并直接赋值给易变变量的二维向量,而是通过视线及法向量计算出来的三维向量,代表视线经表面反射后的方向。
(7)完成了立方图纹理相关顶点着色器的开发后,就可以开发对应的片元着色器了,其代码如下。
代码位置:见随书光盘中源代码/第3章/Sample3_1/assets目录下的frag_tex_cube.sh。
1 precision mediump float; 2 uniform samplerCube sTexture; //纹理内容数据 3 varying vec3 vTextureCoord; //接收从顶点着色器过来的参数 4 void main(){ 5 //通过传入的采样向量与立方图纹理调用textureCube方法执行采样 6 gl_FragColor=textureCube(sTexture, vTextureCoord); 7 }
说明
从上述代码中可以看出,由于OpenGL ES 2.0着色语言内置函数的良好支持,得到采样向量后的采样工作就非常简单了,直接调用textureCube函数即可完成。
到这里为止如何采用立方图纹理技术实现反射环境模拟就介绍完了,在以后的具体项目开发中读者若能做到灵活运用,将可以开发出更加逼真的场景。