第5章 愿君多采撷,此物最相思——光照效果的开发
光照让世界有了颜色,让世界更加绚丽。想象一下,在一个阳光明媚的早晨和在一个多云、灰蒙蒙的日子里看到的大海的景色肯定是截然不同的。阳光明媚的早晨看到的大海一定是淡蓝色的,而多云的日子看到的大海也肯定是灰蒙蒙的。
本章将向读者介绍OpenGL ES里的光照,本章与之前章节可能略有不同,在给出效果例子之前会向读者介绍OpenGL ES中光照的相关基础知识,读者应该认真阅读,不然在看效果例子时一定是一头雾水。
5.1 五彩缤纷的源泉——光源
本小节将向读者介绍关于光源的相关知识,对于以前没有接触过这方面知识的读者来说,可能会觉得有些不知所云。读者需反复阅读这一章节的知识,然后结合本章例子体会。
5.1.1 光源的开启及关闭
在介绍如何打开及关闭光源之前,笔者先带领读者对比一下打开灯光效果与关闭灯光效果的区别。如图5-1(a)所示为打开灯光的白色球体的效果,如图5-1(b)所示为没有灯光效果的白色球体。
提示:如果没有光照,绝大多数物体看上去甚至不像三维物体,图5-1(b)中就是没有灯光效果的白色球体,但它看上去更像是一个白色的圆形。
在Android中开启光源与关闭光源的方法很简单,通过方法gl.glEnable (GL10.GL_LIGHTING)可以打开光源,通过方法gl.glDisable(GL10.GL_LIGHTING)可以关闭灯光效果。但是如果只启动了gl.glEnable(GL10.GL_LIGHTING),而没有进行其他设置,则画面会近似全黑,仔细观看屏幕会有物体的一点点印记。
提示:本小节中的例子,笔者将在本章后面给出。读者应重点理解理论知识,然后再结合例子,理论联系实践,使自己更加精进。
图5-1(a) 打开灯光的白色球体的效果图
图5-1(b) 没有打开灯光的白色球体效果图
5.1.2 设定光源的数量
OpenGL ES的场景中至少可以包含8个光源,它们可以是不同的颜色。除了0号灯之外的其他光源的颜色是黑色。
通过函数gl.glEnable()可以设定打开某盏灯,而方法的参数就是GL_LIGHT0、GL_LIGHT1,……,或GL_LIGHT7,即OpenGL ES中的8盏灯。如图5-2所示为开启了不同灯后,白色球体的效果。
图5-2(a) 开启2灯效果
图5-2(b) 开启3灯效果
图5-2(c) 开启5灯效果
提示:本例子会在本章之后的章节中给出,读者可以通过改变RatingBar来改变灯光的数量。在本例中,最多可以开启5盏灯。
图5-2(a)为开启了白色灯光与红色灯光的效果图,红色灯光与白色灯光的叠加处依然为红色,而这似乎与自然世界是一样的。
再看图5-2(b),为开启了白色、红色及蓝色灯光的效果,红蓝两色叠加处呈现出紫色。图5-2(c)为开启了红、绿、蓝、黄及白色灯光后的效果,读者可以发现球体的大部分已经被灯光渲染成了白色,而这又再次验证了我们刚刚的结论,OpenGL ES中灯光的效果与自然世界近似一样。
当然,在启动了某盏灯以后,如果不对灯进行相关设置,一样达不到我们要的效果,本章接下来几个小节将带领读者一步步走向“光明的世界”。
5.1.3 定向光
我们日常所见的的光源有很多,比如太阳、灯泡、燃烧着的蜡烛等。本节主要讲述像太阳这类被认为是从无穷远处发射的几乎平行的光——定向光。
定向光对应的是光源在无穷远处的光,定向光在空间中的所有的位置方向都是相同的。如图5-3所示为在Android中模拟的定向光照射的场景效果。可以看出,场景中的光是平行的且方向相同。
图5-3(a) 水平斜向下效果
图5-3(b) 竖直向下效果
图5-3(c) 水平斜向下效果
提示:本演示程序通过改变SeekBar的值来改变定向光x坐标的方向。通过该程序,读者可以将该程序与下一小节的定位光的程序做对比,来体会不同光源的不同效果。
OpenGL ES中是通过glLightfv (int light,int pname,float[] params,int offset)方法来设定定向光的,下面对其各个参数一一进行介绍。
● light:该参数设定为OpenGL ES中的灯,与上一小节中的灯一样,用GL_LIGHT0到GL_LIGHT7来表示8盏灯。如果该处设置的为GL_LIGHT0,即表示glLightfv这个方法中其余的设置都是针对GL_LIGHT0的,即0号灯进行设置的。
● pname:被设置的光源的属性是由pname定义的,它指定了一个命名参数,如表5-1所示。在设置定向光时,应该设置成GL_POSITION。
表5-1 光源设置参数表
注意: 表5-1所列的GL_LIGHT0的GL_DIFFUSE、GL_SPECULAR的默认值和其他光源(GL_LIGHT1、GL_LIGHT2等)不同。对于GL_LIGHT0的GL_DIFFUSE、GL_SPECULAR的默认值均为(1.0,1.0,1.0,1.0),而其他光源的默认值为(0.0,0.0,0.0,1.0)。
● params:这个参数是一个float数组,该数组由4部分组成,前3个值组成表示定向光方向的向量,光的方向为从向量点处向原点处照射。如{0,1,0,0}表示沿Y轴负方向的光。最后的0表示此光源发出的是定向光。
注意: 如果向量点的位置为[0,0,0],则光方向向量的起点与终点重合,此时不能确定光线的方向,实际运行时画面为黑色的,看不到光亮。
● offset:为偏移量,设置为0表示第1个值在数组中的偏移量为0。
5.1.4 定位光
上一小节介绍了定向光,本小节介绍OpenGL ES中的定位光。在自然世界中定向光与定位光是截然不同的,这就像太阳与燃烧的蜡烛之间的区别。但是,在OpenGL ES中,定向光与定位光的实现却十分相似。
在介绍定位光的实现之前,笔者先带领读者感性上体会一下定位光。如图5-4所示为定位光在不同位置处照射物体的效果。
图5-4(a) 定位光在3个球体之间示意图
图5-4(b) 定位光在3个球体前面示意图
提示:3个球体为同一球体在不同位置处绘制的,中间的球体看起来较小,是因为其Z轴坐标较小,而场景使用了透视投影,所以会有近大远小的效果。屏幕上的SeekBar是改变灯光的Z轴位置。
OpenGL ES中通过glLightfv (int light,int pname,float[] params,int offset)方法来设定定位光,读者可能会发现,这个参数与上一小节的定向光的设置是同一个方法,而且里面的参数基本都相同,仅仅是params参数略有不同,下面一一列举。
● 在定向光中,params参数的最后一个参数设置为0,而在定位光中,该参数设定为1。
● 在定向光中,params参数的前3个参数为设定光源的向量坐标,而在定位光中,这3个参数是光源的位置。
● 在定向光中光的方向为给定的坐标点与原点之间的向量,所以params中的坐标不能设置为[0,0,0],而在定位光中给出的是光源的坐标位置,所以params前3个参数可以设置为[0,0,0]。
提示:glLightfv()中其余参数的设置与上一小节相同,这里就不再赘述了。
5.2 光源的颜色
上一节介绍了光源的位置及方向,本节将带领读者认识光源的另一种重要属性——光源的颜色。OpenGL ES允许把与颜色相关的3个不同参数GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR与任何特定的光源相关联。
5.2.1 环境光
本小节介绍GL_ AMBIENT,首先带领大家从感性上认识一下光源的3个参数的效果,如图5-5所示为与光照相关的3个不同参数的效果。
图5-5 ambient、diffuse、specular效果图
ambient即环境光,表示一个特定的光源在场景中所添加的环境光的RGBA强度。在表5-1中可以看到,在默认情况下是不存在环境光的,因为GL_ AMBIENT的默认值是(0.0,0.0,0.0,1.0)。
在OpenGL ES中通过glLightfv (int light,int pname,float[] params,int offset)方法来设定光源的环境光,下面对其各个参数一一进行介绍。
● light:该参数设定为OpenGL ES中的灯,用GL_LIGHT0到GL_LIGHT7来表示8盏灯。如果该处设置的为GL_LIGHT0,即表示glLightfv这个方法中其余的设置都是针对GL_LIGHT0,即0号灯进行设置的。
● pname:被设置的光源的属性是由pname定义的,对于环境光设置为GL_AMBIENT。
● params:此参数给出的是灯光颜色的R、G、B、A 4个色彩通道的值,一般环境光设置的值均较小。
● offset:偏移量,设置为0,表示第1个色彩通道的值在数组中的偏移量。
5.2.2 散射光
散射光的效果如图5-5所示,散射光来自于某个方向。因此,如果散射光从正面照射物体表面,它看起来就显得更亮一些。反之,如果它斜着从物体表面掠过,则看起来就显得暗一些。但是当散射光撞击物体表面时,它就会向四面八方均匀地发散。
不管从哪个方向看,散射光看上去总是一样亮。来自某个特定位置或方向的任何光很可能具有散射成分。
OpenGL ES中通过glLightfv (int light,int pname,float[] params,int offset)方法来设定光源的散射光,下面对其各个参数一一进行介绍。
● light:该参数设定为OpenGL ES中的灯,用GL_LIGHT0到GL_LIGHT7来表示8盏灯。如果该处设置为GL_LIGHT0,即表示glLightfv这个方法中其余的设置都是针对GL_LIGHT0,即0号灯进行设置的。
● pname:被设置的光源的属性是由pname定义的,对于环境光设置为GL_ DIFFUSE。
● params:此参数给出的是灯光颜色的R、G、B、A 4个色彩通道的值。
● offset:偏移量,设置为0,表示第1个色彩通道的值在数组中的偏移量。
5.2.3 镜面反射光
镜面光的效果如图5-5所示,其来自一个特定的方向,并且倾向于从表面向某个特定的方向反射。镜面光肉眼看起来是物体上最亮的地方。当然这与物体本身也有关系,如果是类似镜子的、光泽的金属等,则光线为全反射,整个物体都很明亮,而对于像石膏雕像、地毯等,则几乎不存在镜面成分。
OpenGL ES中通过glLightfv (int light,int pname,float[] params,int offset)方法来设定光源的镜面光,下面对其各个参数一一进行介绍。
● light:该参数设定为OpenGL ES中的灯,用GL_LIGHT0到GL_LIGHT7来表示8盏灯。如果该处设置的为GL_LIGHT0,即表示glLightfv这个方法中其余的设置都是针对GL_LIGHT0,即0号灯进行设置的。
● pname:被设置的光源的属性是由pname定义的,对于环境光设置为GL_SPECULAR。
● params:此参数给出的是灯光颜色的R、G、B、A 4个色彩通道的值。在镜面反射光中,该参数一般较大。
● offset:偏移量,设置为0,表示第1个色彩通道的值在数组中的偏移量。
5.3 材料反光属性的法官——法向量及材质
日常生活中,人们能看到桌子上的水杯,是因为光照从水杯上反射回来,进入人们的眼睛。所以决定人们看到桌面上的水杯的,除了光本身的性质,还有物体本身的属性。
在OpenGL ES中决定物体光线反射的有物体各个顶点的法向量,以及物体的材质。
5.3.1 法向量的作用
物体每个顶点的法向量决定了光照时的反射情况,如果没有为顶点设置法向量,光照系统也是不能正常工作的。物体每个顶点的法向量是指处于三维物体表面处的法线方向,其用一个向量来表示。OpenGL ES中向量用一个三元组来表示,格式为(x,y,z)。其表示的是从原点O(0,0,0)出发到(x,y,z)点的一个向量,代表一个方向,如图5-6所示。
图5-6 法向量的坐标表示示意图
某个顶点的法向量三元组与某坐标系三元组并没有必然的逻辑联系。开发中注意不要混淆法向量三元组与坐标三元组。球面上某点的法向量就是球心与此点连线构成的向量。若球心位于原点O,则球面上点的法向量三元组与此点的三维坐标三元组是相同的,如图5-7所示。
图5-7 圆的顶点及法向量坐标示意图
提示:在球面上处处连续可微,因此每个顶点都是可以方便地计算出法向量的。而很多三维物体的表面虽然是处处连续的,但是某些特定的顶点处不可微,这时就不能直接计算出法向量了。
5.3.2 光照的开启及关闭例子
终于熬过了枯燥的理论阶段,本小节将带领读者编写本章光照的开启及关闭一节给出的演示程序。
(1)打开Eclipse,导入名为Sample5_1的项目。
(2)然后向读者介绍layout目录下的main.xml文件。
代码位置:本书随书光盘中源代码\第5章\Sample5_1\res\layout\main.xml。
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:orientation="vertical" 4 android:layout_width="fill_parent" 5 android:layout_height="fill_parent" 6 android:id="@+id/lla" 7 > <!-- LinearLayout布局--> 8 <ToggleButton 9 android:textOff="单击关闭灯光效果" 10 android:textOn="单击打开灯光效果" 11 android:checked="true" 12 android:id="@+id/ToggleButton01" 13 android:layout_width="fill_parent" 14 android:layout_height="wrap_content"> 15 </ToggleButton> <!--添加ToggleButton --> 16 </LinearLayout>
提示:本段代码中有一个线性布局,在线性布局中添加了一个ToggleButton。
(3)接着向读者介绍Ball.java类。
代码位置:本书随书光盘中源代码\第5章\Sample5_1\src\wyf\zcl\Ball.java。
1 package wyf.zcl; 2 ……//此处省略部分不重要代码,读者可在光盘源代码中查看 3 public class Ball { 4 ……//此处省略部分不重要代码,读者可在光盘源代码中查看 5 public Ball(int scale){ 6 ……//该处代码为创建球体,接下来会详细介绍 7 //创建顶点坐标数据缓冲 8 //vertices.length*4是因为1个整数4个字节 9 ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4); 10 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序 11 mVertexBuffer = vbb.asIntBuffer(); //转换为int型缓冲 12 mVertexBuffer.put(vertices); //向缓冲区放入顶点坐标数据 13 mVertexBuffer.position(0); //设置缓冲区起始位置 14 //创建顶点法向量数据缓冲 15 //vertices.length*4是因为1个float 4个字节 16 ByteBuffer nbb = ByteBuffer.allocateDirect(vertices.length*4); 17 nbb.order(ByteOrder.nativeOrder()); //设置字节顺序 18 mNormalBuffer = vbb.asIntBuffer(); //转换为int型缓冲 19 mNormalBuffer.put(vertices); //向缓冲区放入顶点坐标数据 20 mNormalBuffer.position(0); //设置缓冲区起始位置 21 //特别提示:由于不同平台字节顺序不同,数据单元不是字节的一定要经过ByteBuffer 22 //转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题 23 //顶点坐标数据的初始化================end======================= 24 //三角形构造索引数据初始化==========begin======================== 25 ArrayList<Integer> alIndex=new ArrayList<Integer>(); 26 int row=(180/angleSpan)+1; //球面切分的行数 27 int col=360/angleSpan; //球面切分的列数 28 //该处代码为球体顶点索引,接下来会详细介绍 29 iCount=alIndex.size(); 30 byte indices[]=new byte[alIndex.size()]; 31 for(int i=0;i<alIndex.size();i++){ 32 indices[i]=alIndex.get(i).byteValue(); 33 } 34 //创建三角形构造索引数据缓冲 35 mIndexBuffer = ByteBuffer.allocateDirect(indices.length); 36 mIndexBuffer.put(indices); //向缓冲区放入三角形构造索引数据 37 mIndexBuffer.position(0); //设置缓冲区起始位置 38 //三角形构造索引数据初始化==========end========================== 39 } 40 public void drawSelf(GL10 gl){ 41 gl.glRotatef(mAngleZ, 0, 0, 1); //沿Z轴旋转 42 gl.glRotatef(mAngleX, 1, 0, 0); //沿X轴旋转 43 gl.glRotatef(mAngleY, 0, 1, 0); //沿Y轴旋转 44 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); 45 gl.glEnableClientState(GL10.GL_NORMAL_ARRAY); 46 //为画笔指定顶点坐标数据 47 gl.glVertexPointer( 48 3, //每个顶点的坐标数量为3 49 GL10.GL_FIXED, //顶点坐标值的类型为GL_FIXED 50 0, //连续顶点坐标数据之间的间隔 51 mVertexBuffer //顶点坐标数据 52 ); 53 //为画笔指定顶点法向量数据 54 gl.glNormalPointer(GL10.GL_FIXED, 0, mNormalBuffer); 55 //绘制图形 56 gl.glDrawElements( 57 GL10.GL_TRIANGLES, //以三角形方式填充 58 iCount, //一共icount/3个三角形,iCount个顶点 59 GL10.GL_UNSIGNED_BYTE, //索引值的尺寸 60 mIndexBuffer //索引值数据 61 ); }}
● 第9~13行为创建顶点坐标数据缓冲,由于不同平台字节顺序不同,数据单元不是字节的一定要经过ByteBuffer转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题。
● 第16~20行为创建顶点法向量数据缓冲,与顶点缓冲区一样,法向量数据缓冲一样要经过ByteBuffer转换。
● 第24~38行为三角形构造索引数据初始化,本例球的构建是使用索引法。
● 第41~43行分别设置了可以沿Z轴旋转、沿X轴旋转及沿Y轴旋转。
● 第44~45行分别开启了顶点坐标数组,以及顶点法向量坐标数组。如果要使用光照,则一定要开启定点法向量坐标数组。
● 第46~61行为画笔指定顶点坐标数据、顶点法向量数据,同时绘制图形。通过函数glNormalPointer()为画笔指定顶点法向量数据。
(4)接下来介绍第6行处省略的球体的代码。
代码位置:本书随书光盘中源代码\第5章\Sample5_1\src\wyf\zcl\Ball.java。
1 for(int vAngle=-90;vAngle<=90;vAngle=vAngle+angleSpan){ //垂直方向angleSpan度一份 2 for(int hAngle=0;hAngle<360;hAngle=hAngle+angleSpan) //水平方向angleSpan度一份 3 { //纵向横向各到一个角度后计算对应的点在球面上的坐标 4 double xozLength=scale*UNIT_SIZE*Math.cos(Math.toRadians(vAngle)); 5 int x=(int)(xozLength*Math.cos(Math.toRadians(hAngle))); 6 int z=(int)(xozLength*Math.sin(Math.toRadians(hAngle))); 7 int y=(int)(scale*UNIT_SIZE*Math.sin(Math.toRadians(vAngle))); 8 //将计算出来的x、y、z坐标加入存放顶点坐标的ArrayList 9 alVertix.add(x);alVertix.add(y);alVertix.add(z); 10 }}
● 第5行,计算球体的x坐标,球体的x坐标计算公式为:x=a*cosn*cosm。其中n为纬度,m为经度,a为椭球体上的长轴。
● 第6行,计算球体的 y 坐标,球体的 y 坐标计算公式为:y=b*cosn*sin m。其中 n为纬度,m为经度,b为椭球体上的短轴。
● 第7行,计算球体的z坐标,球体的z坐标计算公式为:z=c*sinn。其中n为纬度,c为椭球体上的纵轴。
(5)接下来介绍第28行,球体顶点索引的代码。
代码位置:本书随书光盘中源代码\第5章\Sample5_1\src\wyf\zcl\Ball.java。
1 for(int i=0;i<row;i++){ //对每一行循环 2 if(i>0&&i<row-1){ //对中间行进行判断 3 //中间行 4 for(int j=-1;j<col;j++){ //对列进行循环 5 int k=i*col+j; 6 alIndex.add(k+col); //添加项 7 alIndex.add(k+1); 8 alIndex.add(k); 9 } 10 for(int j=0;j<col+1;j++){ //对列进行循环 11 int k=i*col+j; 12 alIndex.add(k-col); //添加项 13 alIndex.add(k-1); 14 alIndex.add(k); 15 }}} 16 iCount=alIndex.size(); //获得iCount值 17 byte indices[]=new byte[alIndex.size()]; //创建存储顶点索引的数组 18 for(int i=0;i<alIndex.size();i++){ 19 indices[i]=alIndex.get(i).byteValue(); //添加索引数据 20 }
● 第4~9行,中间行的两个相邻点与下一行的对应点构成三角形。
● 第10~14行,中间行的两个相邻点与上一行的对应点构成三角形。
(6)然后打开src目录下的MyActivity.java文件,将源文件替换为如下代码。
代码位置:本书随书光盘中源代码\第5章\Sample5_1\src\wyf\zcl\MyActivity.java。
1 package wyf.zcl; 2 import android.app.Activity; //引入相关包 3 import android.os.Bundle; 4 import android.widget.CompoundButton; 5 import android.widget.LinearLayout; 6 import android.widget.ToggleButton; 7 import android.widget.CompoundButton.OnCheckedChangeListener; 8 public class MyActivity extends Activity { //创建MyActivity并继承Activity类 9 //Activity创建时调用 10 MySurfaceView msv; //界面对象声明 11 ToggleButton tb; //按钮声明 12 @Override 13 public void onCreate(Bundle savedInstanceState) { 14 super.onCreate(savedInstanceState); 15 msv=new MySurfaceView(this); //实例化MySurfaceView 16 setContentView(R.layout.main); //设置Acitivity的内容 17 msv.requestFocus(); //获取焦点 18 msv.setFocusableInTouchMode(true); //设置为可触控 19 LinearLayout lla=(LinearLayout)findViewById(R.id.lla); 20 lla.addView(msv); //将SurfaceView加入LinearLayout中 21 tb=(ToggleButton)findViewById(R.id.ToggleButton01); //添加监听器 22 tb.setOnCheckedChangeListener(new OnCheckedChangeListener(){ 23 @Override 24 public void onCheckedChanged(CompoundButton buttonView, 25 boolean isChecked) { 26 msv.openLightFlag=!msv.openLightFlag; 27 }});} 28 @Override 29 protected void onPause() { //当另一个Acitvity遮挡当前的Activity时调用 30 super.onPause(); 31 msv.onPause(); 32 } 33 @Override 34 protected void onResume() { //当Acitvity获得用户焦点时调用 35 super.onResume(); 36 msv.onResume(); 37 }}
● 第15~18行,实例化MySurfaceView对象,同时设置Acitivity的内容,并且设置MySurfaceView为可触控。
● 第19~27行,在LinearLayout中添加SurfaceView,同时为切换按钮添加监听器,以控制开灯与关灯。
● 第28~37行,为Activity的另外两个声明周期函数,当Acitvity调用了onPause()及onResume() 时,GLSurfaceView应该调用相应的操作,即调用onPause()及onResume()。
(7)新建名为MySurfaceView.java类的文件,将源文件替换为如下代码。
代码位置:本书随书光盘中源代码\第5章\Sample5_1\src\wyf\zcl\MySurfaceView.java。
1 package wyf.zcl; 2 ……//此处省略部分不重要代码,读者可在光盘源代码中查看 3 public class MySurfaceView extends GLSurfaceView{ 4 ……//此处省略部分不重要代码,读者可在光盘源代码中查看 5 boolean openLightFlag=false; //开灯标记,false为关灯,true为开灯 6 public MySurfaceView(Context context) {...}//由于篇幅有限,该处省略一些代码 7 @Override public boolean onTouchEvent(MotionEvent e) {...} //触摸事件回调方法 8 private class SceneRenderer implements GLSurfaceView.Renderer { 9 Ball ball=new Ball(4); 10 public SceneRenderer(){ 11 } 12 public void onDrawFrame(GL10 gl){ 13 gl.glShadeModel(GL10.GL_SMOOTH); 14 if(openLightFlag){ //开灯 15 gl.glEnable(GL10.GL_LIGHTING); //允许光照 16 initLight0(gl); //初始化绿色灯 17 initMaterialWhite(gl); //初始化材质为白色 18 float[] positionParamsGreen={2,1,0,1}; //最后的1表示是定位光 19 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, positionParamsGreen,0); 20 }else{ //关灯 21 gl.glDisable(GL10.GL_LIGHTING); 22 } 23 gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);//清除颜色缓存 24 gl.glMatrixMode(GL10.GL_MODELVIEW); //设置当前矩阵为模式矩阵 25 gl.glLoadIdentity(); //设置当前矩阵为单位矩阵 26 gl.glTranslatef(0, 0f, -1.8f); 27 ball.drawSelf(gl); 28 gl.glLoadIdentity(); 29 } 30 public void onSurfaceChanged(GL10 gl, int width, int height) { 31 gl.glViewport(0, 0, width, height); //设置视窗大小及位置 32 gl.glMatrixMode(GL10.GL_PROJECTION); //设置当前矩阵为投影矩阵 33 gl.glLoadIdentity(); //设置当前矩阵为单位矩阵 34 float ratio = (float) width / height; //计算透视投影的比例 35 gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); //计算产生透视投影矩阵 36 } 37 public void onSurfaceCreated(GL10 gl, EGLConfig config) { 38 gl.glDisable(GL10.GL_DITHER); //关闭抗抖动 39 gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,GL10.GL_FASTEST); //快速模式 40 gl.glClearColor(0,0,0,0); //设置屏幕背景色黑色RGBA 41 gl.glShadeModel(GL10.GL_SMOOTH); //平滑着色 42 gl.glEnable(GL10.GL_DEPTH_TEST); //启用深度测试 43 }} 44 private void initLight0(GL10 gl){ 45 gl.glEnable(GL10.GL_LIGHT0); //打开0号灯 46 float[] ambientParams={0.1f,0.1f,0.1f,1.0f}; //环境光设置 47 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientParams,0); 48 float[] diffuseParams={0.5f,0.5f,0.5f,1.0f}; //散射光设置 49 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffuseParams,0); 50 float[] specularParams={1.0f,1.0f,1.0f,1.0f}; //反射光设置 51 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specularParams,0); 52 } 53 private void initMaterialWhite(GL10 gl){ 54 float ambientMaterial[] = {0.4f, 0.4f, 0.4f, 1.0f}; //环境光为白色材质 55 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, ambientMaterial,0); 56 float diffuseMaterial[] = {0.8f, 0.8f, 0.8f, 1.0f}; //散射光为白色材质 57 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, diffuseMaterial,0); 58 float specularMaterial[] = {1.0f, 1.0f, 1.0f, 1.0f}; //高光材质为白色 59 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, specularMaterial,0); 60 float shininessMaterial[] = {1.5f}; //高光反射区域,数越大高亮区域越小、越暗 61 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, shininessMaterial,0); 62 }}
● 第2~7行,多处省略了相关代码,这部分代码较为简单,由于篇幅有限,进行了酌情省略。读者可以在随书光盘中找到相关代码。
● 第12~29行为图形的绘制函数。通过gl.glEnable(GL10.GL_LIGHTING)函数可以打开灯光效果。通过gl.glLightfv()可以设定光照相关参数,如光源的位置等。
● 第30~36行,当SurfaceView的屏幕改变时调用。这段代码主要是设定屏幕的大小、投影模式等,此段代码较为固定,一般直接复制过来使用。
● 第37~43行,当SurfaceView创建时调用该段代码,在该方法中主要进行如关闭抗抖动、设置背景颜色、设置着色模式等操作。此段代码较为固定,一般直接复制过来再根据需要进行修改。
● 第44~52行,初始化0号灯,设置0号灯的环境光、散射光、反射光。本例中将这几个函数单独封装成一个方法,以方便以后调用,同时增强代码的可读性。
● 第53~62行,设置物体的材质,关于这部分知识,笔者将在本节下一小节中介绍,读者在学习完下一小节的内容后,可返回阅读此处内容。
5.3.3 物体的材质
设置材质的主要工作就是设置材质对于环境光、散射光、镜面光的反射能力,也就是说材质能反射什么颜色的光。例如,白色的材质能反射所有颜色的光,红色的材质只能反射红色光。
通过public abstract void glMaterialfv (int face, int pname, float[] params, int offset)方法可以设置材质的相关属性,下面一一介绍其参数。
● face:设置材质能反光的面,设置为GL10.GL_FRONT_AND_BACK,可以实现正反面均可反射光线。
● pname:如果要设置环境光,则设置参数为GL10.GL_AMBIENT;如果是散射光,则设置参数为GL10.GL_DIFFUSE;如果是镜面光,则设置参数为GL10.GL_SPECULAR。还有一种参数——高光反射区域,通过GL10.GL_SHININESS设置。
● params:对于环境光、散射光、镜面光,该参数设置一个一维四元的float数组,表示R、G、B、A 4个参数。而对于高光反射区域,则设置一个参数,数越大,高亮区域越小、越暗。
● offset:为数组中的偏移量,一般设置为0。
5.3.4 设定光源数量的例子
本章之前小节中给出了一个设定光源数量的例子,本小节将带读者一起编写这个例子,具体步骤如下。
(1)打开Eclipse,导入名为Sample5_2的项目。
(2)然后向读者介绍layout目录下的main.xml文件。
代码位置:本书随书光盘中源代码\第5章\Sample5_2\res\layout\main.xml。
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:orientation="vertical" 4 android:layout_width="fill_parent" 5 android:layout_height="fill_parent" 6 android:id="@+id/lla" 7 > <!--添加一个LinearLayout --> 8 <RatingBar 9 android:id="@+id/RatingBar01" 10 android:layout_width="wrap_content" 11 android:layout_height="wrap_content" 12 android:max="5" 13 android:rating="1" 14 > <!--添加一个RatingBar --> 15 </RatingBar> 16 </LinearLayout>
提示:本段代码中有一个线性布局,在线性布局中添加了一个RatingBar。
(3)接着应该向读者介绍Ball.java类,该类代码与之前例子较为相似,读者可以参看之前绘制球体的例子,或者参看随书光盘中的源代码,这里就不再赘述了。
代码位置:本书随书光盘中源代码\第5章\Sample5_2\src\wyf\zcl\Ball.java。
(4)然后介绍src目录下的MyActivity.java类。
代码位置:本书随书光盘中源代码\第5章\Sample5_2\src\wyf\zcl\MyActivity.java。
1 package wyf.zcl; 2 …... //由于篇幅有限,该处省略一些代码 3 public class MyActivity extends Activity { 4 //Activity创建时调用 5 MySurfaceView msv; 6 RatingBar rb; //声明拖拉条引用 7 @Override 8 public void onCreate(Bundle savedInstanceState) { 9 super.onCreate(savedInstanceState); 10 msv=new MySurfaceView(this); //实例化SurfaceView对象 11 setContentView(R.layout.main); //设置Activity内容 12 rb=(RatingBar)findViewById(R.id.RatingBar01); 13 msv.requestFocus(); //获取焦点 14 msv.setFocusableInTouchMode(true); //设置为可触控 15 LinearLayout lla=(LinearLayout)findViewById(R.id.lla); 16 lla.addView(msv); //向LinearLayout中添加View控件 17 rb.setOnRatingBarChangeListener(new RatingBar.OnRatingBarChangeListener() { 18 @Override 19 public void onRatingChanged(RatingBar ratingBar, float rating, 20 boolean fromUser) { 21 if(rating>=0&&rating<=1){ //以RatingBar的变化来改变开启灯光的数量 22 msv.openLightNum=1; 23 }else if(rating>1&&rating<=2){ 24 msv.openLightNum=2; 25 }else if(rating>2&&rating<=3){ 26 msv.openLightNum=3; 27 }else if(rating>3&&rating<=4){ 28 msv.openLightNum=4; 29 }else if(rating>4&&rating<=5){ 30 msv.openLightNum=5; 31 } 32 Toast.makeText(MyActivity.this, "开启了"+msv.openLightNum+"盏灯", 33 Toast.LENGTH_SHORT).show(); 34 }});} 35 @Override 36 protected void onPause() { //重写onPause方法 37 super.onPause(); 38 msv.onPause(); 39 } 40 @Override 41 protected void onResume() { //重写onResume方法 42 super.onResume(); 43 msv.onResume(); 44 }}
● 第10~11行,实例化MySurfaceView对象,同时设置Acitivity的内容,并且设置MySurfaceView为可触控。
● 第15~34行,在LinearLayout中添加SurfaceView,同时为RatingBar添加监听器,通过改变RatingBar来改变灯打开的个数。
● 第35~44行为Activity的另外两个声明周期函数,当Acitvity调用了onPause()及onResume() 时,GLSurfaceView应该调用相应的操作,即调用onPause()及onResume()。
(5)新建名为MySurfaceView.java类的文件,由于该类代码较长,所以只给出较为重要的部分。
代码位置:本书随书光盘中源代码\第5章\Sample5_2\res\layout\MySurfaceView.java。
首先给出一段材质为白色的,为其设置环境光、散射光、镜面光及高光反射区域的代码。
代码位置:本书随书光盘中源代码\第5章\Sample5_2\src\wyf\zcl\MyActivity.java。
1 private void initMaterialWhite(GL10 gl) 2 {//材质为白色时什么颜色的光照在上面就将体现出什么颜色 3 //环境光为白色材质 4 float ambientMaterial[] = {0.4f, 0.4f, 0.4f, 1.0f}; 5 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, ambientMaterial,0); 6 //散射光为白色材质 7 float diffuseMaterial[] = {0.8f, 0.8f, 0.8f, 1.0f}; 8 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, diffuseMaterial,0); 9 //高光材质为白色 10 float specularMaterial[] = {1.0f, 1.0f, 1.0f, 1.0f}; 11 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, specularMaterial,0); 12 //高光反射区域,数越大,高亮区域越小、越暗 13 float shininessMaterial[] = {1.5f}; 14 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, shininessMaterial,0); 15 }
● 第4~5行,新建环境光float数组,环境光一般设置为较低。本例中材质为白色,所以RGB颜色设置相同,之后将环境光数组添加进入glMaterialfv方法的params参数。设置face参数为GL_FRONT_AND_BACK,设置pname参数为GL10.GL_AMBIENT,设置offset为0。
● 第7~8行,新建散射光float数组,散射光一般设置为比环境光高,比反射光低。本例中材质为白色,所以RGB颜色设置相同。之后将散射光数组添加进入glMaterialfv方法的params参数。设置face参数为GL_FRONT_AND_BACK,设置pname参数为GL10.GL_DIFFUSE,设置offset为0。
● 第10~11行,新建镜面光float数组,镜面光一般设置较高。本例中材质为白色,所以RGB颜色设置相同。之后将散射光数组添加进入glMaterialfv方法的params参数。设置face参数为GL_FRONT_AND_BACK,设置pname参数为GL10.GL_SPECULAR,设置offset为0。
● 第13~14行,新建高光反射区域float数组,高光反射区域数越大,高亮区域越小、越暗。之后将散射光数组添加进入glMaterialfv方法的params参数。设置face参数为GL_FRONT_AND_BACK,设置pname参数为GL10.GL_ SHININESS,设置offset为0。
下面给出灯的设置。
代码位置:本书随书光盘中源代码\第5章\Sample5_2\src\wyf\zcl\MyActivity.java。
1 private void initLight0(GL10 gl){ 2 gl.glEnable(GL10.GL_LIGHT0); //打开0号灯,白色 3 //环境光设置 4 float[] ambientParams={0.1f,0.1f,0.1f,1.0f}; //光参数RGBA 5 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientParams,0); 6 //散射光设置 7 float[] diffuseParams={0.5f,0.5f,0.5f,1.0f}; //光参数RGBA 8 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffuseParams,0); 9 //反射光设置 10 float[] specularParams={1.0f,1.0f,1.0f,1.0f}; //光参数RGBA 11 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specularParams,0); 12 }
● 第2~5行,打开0号灯,同时设置0号灯的环境光属性。新建环境光float数组,环境光一般设置较低。本例中材质为白色,所以RGB颜色设置相同。之后将环境光数组添加进入glLightfv方法的params参数。设置light参数为GL10.GL_LIGHT0,设置pname参数为GL10.GL_AMBIENT,设置offset为0。
● 第7~8行,设置0号灯的散射光属性。散射光一般设置为比环境光高,比反射光低。本例中材质为白色,所以RGB颜色设置相同。之后将散射光数组添加进入glLightfv方法的params参数。设置light参数为GL10.GL_LIGHT0,设置pname参数为GL10.GL_ DIFFUSE,设置offset为0。
● 第10~11行,设置0号灯的镜面光属性。镜面光一般设置较高。本例中材质为白色,所以RGB颜色设置相同。之后将镜面光数组添加进入glLightfv方法的params参数。设置light参数为GL10.GL_LIGHT0,设置pname参数为GL10.GL_ SPECULAR,设置offset为0。
提示:本例中共开启了5盏灯,其余灯的设置与0号灯的设置较为相似,都是通过改变3种光的float数组,来改变颜色的。由于篇幅有限,这里就不一一列举了。
5.4 两个物体发光的例子
纸上谈来终觉浅,本章通过两个精心设计的例子,带领读者一步步编写,更加深刻地理解光照相关知识。
5.4.1 定位光例子的实现
(1)打开Eclipse,导入名为Sample5_3的项目。
(2)然后向读者介绍layout目录下的main.xml文件。
代码位置:本书随书光盘中源代码\第5章\Sample5_3\res\layout\main.xml。
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:orientation="vertical" 4 android:layout_width="fill_parent" 5 android:layout_height="fill_parent" 6 android:id="@+id/lla" 7 > <!--添加一个LinearLayout --> 8 <SeekBar 9 android:id="@+id/SeekBar01" 10 android:layout_width="fill_parent" 11 android:layout_height="wrap_content" 12 android:max="100" 13 android:progress="20" 14 > 15 </SeekBar> <!--添加一个SeekBar --> 16 </LinearLayout>
提示:本段代码中有一个线性布局,在线性布局中添加了一个SeekBar。
(3)接下来应该介绍Ball.java类,该类代码与之前例子较为相似,读者可以参看之前绘制球体的例子,或者参看随书光盘中的源代码,这里就不再赘述了。
代码位置:本书随书光盘中源代码\第5章\Sample5_3\src\wyf\zcl\Ball.java。
(4)然后介绍src目录下的MyActivity.java类。
代码位置:本书随书光盘中源代码\第5章\Sample5_3\src\wyf\zcl\MyActivity.java。
1 package wyf.zcl; 2 ……//此处省略部分不重要代码,读者可在光盘源代码中查看 3 public class MyActivity extends Activity { 4 //Activity创建时调用 5 MySurfaceView msv; 6 SeekBar sb; //声明拖拉条引用 7 @Override 8 public void onCreate(Bundle savedInstanceState) { 9 super.onCreate(savedInstanceState); 10 msv=new MySurfaceView(this); 11 setContentView(R.layout.main); 12 sb=(SeekBar)findViewById(R.id.SeekBar01); 13 msv.requestFocus(); //获取焦点 14 msv.setFocusableInTouchMode(true); //设置为可触控 15 LinearLayout lla=(LinearLayout)findViewById(R.id.lla); 16 lla.addView(msv); 17 sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 18 @Override 19 public void onStopTrackingTouch(SeekBar seekBar) { 20 } 21 @Override 22 public void onStartTrackingTouch(SeekBar seekBar) { 23 } 24 @Override 25 public void onProgressChanged(SeekBar seekBar, int progress, 26 boolean fromUser) { //通过SeekBar来改变定向光的方向 27 System.out.println(progress); 28 if(progress<50){ //光线在左边 29 msv.light0PositionX=-(progress/5); 30 }else if(progress>=50){ //光线在右边 31 msv.light0PositionX=((progress-50)/5); 32 }}});} 33 @Override 34 protected void onPause() { //当另一个Acitvity遮挡当前的Activity时调用 35 super.onPause(); 36 msv.onPause(); 37 } 38 @Override 39 protected void onResume() { //当Acitvity获得用户焦点时调用 40 super.onResume(); 41 msv.onResume(); 42 }}
● 第10~14行,实例化MySurfaceView对象,同时设置Acitivity的内容,并且设置MySurfaceView为可触控。
● 第15~32行,在LinearLayout中添加SurfaceView,同时为SeekBar添加监听器,通过改变SeekBar来改变光源的Z轴坐标,来达到不同的效果。
● 第33~42行为Activity的另外两个声明周期函数,当Acitvity调用了onPause()及onResume() 时,GLSurfaceView应该调用相应的操作,即调用onPause()及onResume()。
(5)接着介绍MySurfaceView.java类的代码内容,由于该类代码较长,所以只给出较为重要的一部分。
代码位置:本书随书光盘中源代码\第5章\Sample5_3\src\wyf\zcl\MySurfaceView.java。
1 package wyf.zcl; 2 ……//此处省略部分不重要代码,读者可在光盘源代码中查看 3 public class MySurfaceView extends GLSurfaceView{ 4 ……//此处省略部分不重要代码,读者可在光盘源代码中查看 5 public MySurfaceView(Context context) { 6 super(context); 7 mRenderer = new SceneRenderer(); //创建场景渲染器 8 setRenderer(mRenderer); //设置渲染器 9 setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); //渲染模式为主动渲染 10 } 11 //触摸事件回调方法 12 @Override public boolean onTouchEvent(MotionEvent e) { 13 float y = e.getY(); 14 float x = e.getX(); 15 switch (e.getAction()) { 16 case MotionEvent.ACTION_MOVE: 17 float dy = y - mPreviousY; //计算触控笔y位移 18 float dx = x - mPreviousX; //计算触控笔x位移 19 mRenderer.ball.mAngleX += dy * TOUCH_SCALE_FACTOR;//设置沿X轴的旋转角度 20 mRenderer.ball.mAngleZ += dx * TOUCH_SCALE_FACTOR;//设置沿Z轴的旋转角度 21 requestRender(); //重绘画面 22 } 23 mPreviousY = y; //记录触控笔位置 24 mPreviousX = x; //记录触控笔位置 25 return true; 26 } 27 private class SceneRenderer implements GLSurfaceView.Renderer { 28 Ball ball=new Ball(3); 29 public SceneRenderer(){} 30 public void onDrawFrame(GL10 gl){ 31 gl.glShadeModel(GL10.GL_SMOOTH); 32 gl.glEnable(GL10.GL_LIGHTING); //允许光照 33 initMaterialWhite(gl); //初始化材质为白色 34 gl.glDisable(GL10.GL_LIGHT0); //每次绘制前取消已开启的灯光效果 35 initLight0(gl);//初始化0号灯 36 float[] positionParams0={0,0,light0PositionX,1};//最后的1表示是定位光,此为0号灯位置参数 37 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, positionParams0,0); //0号灯位置 38 //清除颜色缓存 39 gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); 40 //设置当前矩阵为模式矩阵 41 gl.glMatrixMode(GL10.GL_MODELVIEW); 42 //设置当前矩阵为单位矩阵 43 gl.glLoadIdentity(); 44 gl.glTranslatef(-1, 0f, -1.8f); //平移 45 ball.drawSelf(gl); //绘制 46 gl.glLoadIdentity(); //恢复矩阵 47 gl.glTranslatef(1, 0f, -1.8f) //平移 48 ball.drawSelf(gl); //绘制 49 gl.glLoadIdentity(); //恢复矩阵 50 gl.glTranslatef(0, 0f, -2.8f); //平移 51 ball.drawSelf(gl); //绘制 52 } 53 public void onSurfaceChanged(GL10 gl, int width, int height) { 54 gl.glViewport(0, 0, width, height); //设置视窗大小及位置 55 gl.glMatrixMode(GL10.GL_PROJECTION); //设置当前矩阵为投影矩阵 56 gl.glLoadIdentity(); //设置当前矩阵为单位矩阵 57 float ratio = (float) width / height; //计算透视投影的比例 58 gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); //调用此方法计算产生透视投影矩阵 59 } 60 public void onSurfaceCreated(GL10 gl, EGLConfig config) { 61 gl.glDisable(GL10.GL_DITHER); //关闭抗抖动 62 //设置特定Hint项目的模式,这里设置为使用快速模式 63 gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,GL10.GL_FASTEST); 64 gl.glClearColor(0,0,0,0); //设置屏幕背景色黑色RGBA 65 //设置着色模型为平滑着色 66 gl.glShadeModel(GL10.GL_SMOOTH);//GL10.GL_SMOOTH GL10.GL_FLAT 67 gl.glEnable(GL10.GL_DEPTH_TEST); //启用深度测试 68 } } 69 private void initLight0(GL10 gl){ //初始化0号灯 70 //此处与本章前一小节例子相应处相似,由于篇幅有限,省略此处 71 } 72 private void initMaterialWhite(GL10 gl){ //初始化材质 73 //此处与本章前一小节例子相应处相似,由于篇幅有限,省略此处 74 }}
● 第1~10行,省略了引入相关包一类的代码,读者可以在随书光盘中看到这部分代码。在MuSurfaceView的构造函数中,通过setRenderer()来创建场景渲染器,通过setRenderMode()设置渲染模式为主动渲染。
● 第12~26行,设置触摸事件回调方法,通过计算触控笔位移来使球体旋转。
● 第30~52行为图形的绘制函数。通过gl.glEnable(GL10.GL_LIGHTING)函数可以打开灯光效果。通过gl.glLightfv()可以设定光照相关参数,如光源的位置等。
● 第53~59行,当SurfaceView的屏幕改变时调用。这段代码主要是设定屏幕的大小、投影模式等,此段代码较为固定,一般直接复制过来使用。
● 第60~68行,当SurfaceView创建时调用该段代码,在该方法中主要进行如关闭抗抖动、设置背景颜色、设置着色模式等操作。此段代码较为固定,一般直接复制过来再根据需要进行修改。
● 第69~74行为初始化0号灯及初始化材质,此处与本章前一小节例子相应处相似,读者可以在随书光盘中找到相关代码,也可以在本章之前小节中找到相关代码。
5.4.2 自发光物体加运动光源的实现
本小节将介绍光照最后一部分知识,物体自发光的实现,笔者将通过一个物体自发光同时实现光源运动的例子,来介绍这部分知识,同时复习以前的知识,下面笔者将详细介绍本例子。
(1)打开Eclipse,导入项目Sample5_4。
(2)然后介绍src目录下的MyActivity.java类。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MyActivity.java。
1 package wyf.zcl; 2 import android.app.Activity; //引入相关包 3 import android.os.Bundle; 4 public class MyActivity extends Activity { 5 MySurfaceView msv; //MySurfaceView引用 6 //Activity创建时调用 7 @Override 8 public void onCreate(Bundle savedInstanceState) { 9 super.onCreate(savedInstanceState); 10 msv=new MySurfaceView(this); //实例化MySurfaceView对象 11 setContentView(msv); //设置Activity内容 12 }}
提示:相信读者对以上内容已经烂熟于心了,该Activity的主要作用就是在创建的时候设置Activity显示的内容为MySurfaceView所指向的对象。
(3)接着应该介绍Ball.java类的代码内容,该类代码与之前例子较为相似,读者可以参看之前绘制球体的例子,或者参看随书光盘中的源代码,这里就不再赘述了。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zd\Ball.java。
(4)下面介绍MySurfaceView.java类的代码内容,由于该类代码较长,笔者将先给出该类的大体结构,然后在下一小节做详细的介绍。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MySurfaceView.java。
1 package wyf.zcl; 2 ……//引入相关包 3 class MySurfaceView extends GLSurfaceView { 4 ……//声明相关引用 5 public MySurfaceView(Context context) { 6 super(context); 7 ……//创建及设置场景渲染器 8 } 9 @Override public boolean onTouchEvent(MotionEvent e) { 10 ……//触摸事件回调方法 11 } 12 private class SceneRenderer implements GLSurfaceView.Renderer { 13 ……//实例化球体对象 14 public SceneRenderer() 15 { 16 ……//球体自动旋转线程 17 } 18 public void onDrawFrame(GL10 gl) { 19 ……//onDrawFrame相关方法 20 } 21 public void onSurfaceChanged(GL10 gl, int width, int height) { 22 ……//onSurfaceChanged相关方法 23 } 24 public void onSurfaceCreated(GL10 gl, EGLConfig config) { 25 ……//onSurfaceCreated相关方法 26 }} 27 private void initGreenLight(GL10 gl){ 28 ……//初始化0号灯,绿色 29 } 30 private void initRedLight(GL10 gl){ 31 ……//初始化1号灯,红色 32 } 33 private void initMaterial(GL10 gl){ 34 ……//初始化材质为白色,同时设置材质为自发光 35 }}
● 第2~3行,引入相关包,然后让刚刚创建的类继承自GLSurfaceView,初始化、绘图的工作均是在GLSurfaceView中完成的。而GLSurfaceView的全部功能就是专注于表现OpenGL ES的渲染。
● 第5~8行,创建及设置场景渲染器,同时设置渲染模式为主动渲染。这部分工作一定是在GLSurfaceView的子类的构造方法中完成的。
● 第9~11行为触摸事件回调方法,这段代码实现了用户每次触摸屏幕,则在GLSurfaceView的子类中图形X、Y、Z坐标的变化,即可以让物体平移或选择。本程序中只写了物体选择,有兴趣的读者可以自行修改。
● 第12行,在5~8行中已经提到了场景渲染器,而在初始化GLSurfaceView的时候,程序员必须使用setRenderer(Renderer)方法来设置渲染器。而本行就是一个实现了GLSurfaceView.Renderer接口的类。而该类就是在GLsurfaceView中创建一个渲染OpenGL ES的窗体。
● 第13行,实例化球体对象,对于这些读者应该已经烂熟于心,在之后给出各部分细节代码时,将省略掉该部分,读者可以自行查阅光盘中的源代码。
● 第14~17行为SceneRenderer的构造方法,笔者在该方法中加入了一个内部类,用来实现球体的转动。
● 第18~26行,如果实现了GLSurfaceView.Renderer接口,就必须实现onDrawFrame、onSurfaceChanged、onSurfaceCreated方法。这3处在接下来的代码细节讲解中会进行详细解释。
● 第27~35行,初始化0号灯、1号灯,及材质的颜色,同时设置材质为自发光。
5.4.3 MySurfaceView.java详解
下面将对MySurfaceView.java中每一处的实现进行详细的讲解。
(1)首先是MySurfaceView构造方法的实现,这部分内容很少,但是却是必不可少的。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MySurfaceView.java。
1 public MySurfaceView(Context context) { 2 super(context); 3 mRenderer = new SceneRenderer(); //创建场景渲染器 4 setRenderer(mRenderer); //设置渲染器 5 setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); //设置渲染模式为主动渲染 6 }
● 第3行,创建场景渲染器,即实例化了SceneRenderer对象。
● 第4行,通过setRenderer方法来设置GLSurfaceView的渲染器,该部分工作一定是在GLSurfaceView的构造方法中完成的。
● 第5行,通过setRenderMode方法设置渲染模式为主动渲染,关于Render中的各种渲染模式,在API中有详细解释,读者可以自行查阅。
(2)触摸事件回调方法的实现。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MySurfaceView.java。
1 @Override public boolean onTouchEvent(MotionEvent e) { 2 float y = e.getY(); //获得用户在屏幕上的触控点 3 float x = e.getX(); //获得用户在屏幕上的触控点 4 switch (e.getAction()) { 5 case MotionEvent.ACTION_MOVE: 6 float dy = y - mPreviousY; //计算触控笔y位移 7 float dx = x - mPreviousX; //计算触控笔x位移 8 mRenderer.ball.mAngleX += dy * TOUCH_SCALE_FACTOR;//设置沿X轴的旋转角度 9 mRenderer.ball.mAngleZ += dx * TOUCH_SCALE_FACTOR;//设置沿Z轴的旋转角度 10 requestRender(); //重绘画面 11 } 12 mPreviousY = y; //记录触控笔位置 13 mPreviousX = x; //记录触控笔位置 14 return true; 15 }
● 第2~3行,通过MotionEvent的getY、getX方法,可以获得用户在屏幕上的触控点。
● 第6~7行,计算触控笔y位移及x位移,该处通过本次触摸点坐标与上次触摸点坐标的差来得到位移的变化量。
● 第8~9行,设置沿X轴的旋转角度及设置沿Z轴的旋转角度,该处通过位移变化量的差值,来设置选择的角度。
● 第10行,重绘画面,此段代码调用onDrawFrame方法,进行画面的重绘,如果没有此段代码,那么画面将不会有所改变。
● 第12~13行,记录触控笔X位置及Y位置,为以后调用。
(3)球体自动旋转线程的实现。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MySurfaceView.java。
1 public SceneRenderer(){ //开启一个线程自动旋转球体 2 new Thread(){ 3 public void run(){ //重写线程run方法 4 try{ Thread.sleep(100);} //每次旋转睡眠100毫秒 5 catch(Exception e){e.printStackTrace();} 6 while(true){ 7 lightAngleGreen+=5; //改变绿灯的角度 8 lightAngleRed+=5; //改变红灯的角度 9 requestRender(); //重绘画面 10 try{ Thread.sleep(50);} 11 catch(Exception e){e.printStackTrace();} 12 }}}.start(); 13 }
● 第2~5行为一个匿名内部类,其功能是开启一个线程自动旋转球体,及休息100毫秒。
● 第7~8行,让红色灯及绿色灯角度每次自加5。以达到灯选择的效果。
● 第9行,重绘画面。此段代码调用onDrawFrame方法,进行画面的重绘,如果没有此段代码,那么画面将不会有所改变。
● 第10~12行,休息50毫秒,及启动线程。
(4)onDrawFrame方法详细讲解。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MySurfaceView.java。
1 public void onDrawFrame(GL10 gl) { 2 gl.glShadeModel(GL10.GL_SMOOTH); 3 //设定绿色光源的位置 4 float lxGreen=(float)(7*Math.cos(Math.toRadians(lightAngleGreen))); 5 float lzGreen=(float)(7*Math.sin(Math.toRadians(lightAngleGreen))); 6 float[] positionParamsGreen={lxGreen,0,lzGreen,1}; //最后的1表示使用定位光 7 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, positionParamsGreen,0); 8 //设定红色光源的位置 9 float lyRed=(float)(7*Math.cos(Math.toRadians(lightAngleRed))); 10 float lzRed=(float)(7*Math.sin(Math.toRadians(lightAngleRed))); 11 float[] positionParamsRed={0,lyRed,lzRed,1}; //最后的1表示使用定位光 12 gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_POSITION, positionParamsRed,0); 13 //清除颜色缓存 14 gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); 15 //设置当前矩阵为模式矩阵 16 gl.glMatrixMode(GL10.GL_MODELVIEW); 17 //设置当前矩阵为单位矩阵 18 gl.glLoadIdentity(); 19 gl.glTranslatef(0, 0f, -1.8f); 20 ball.drawSelf(gl); 21 }
● 第2行,设置模式为平滑着色。平滑着色是默认设置,本段代码出于演示目的,将其加入。如果读者删掉这段代码,效果将没有改变。
● 第4~5行,设置绿色光源的位置,光源的运动是按照圆形轨迹运动的。
● 第6~7行,设置0号灯位置,前3个参数表示物体的X、Y、Z坐标,最后的1表示使用定位光,将位置坐标加入glLightfv函数。设置light参数为GL10.GL_LIGHT0,设置pname参数为GL10.GL_ POSITION,offset设置为0。
● 第9~10行,设置红色光源的位置,光源的运动是按照圆形轨迹运动的。
● 第11~12行,设置1号灯位置,前3个参数表示物体的X、Y、Z坐标,最后的1表示使用定位光,将位置坐标加入glLightfv函数。设置light参数为GL10.GL_LIGHT1,设置pname参数为GL10.GL_ POSITION,设置offset为0。
● 第14~18行,清除颜色缓存、设置当前矩阵为模式矩阵,同时设置当前矩阵为单位矩阵。
● 第19~20行,平移图形,这部分知识在以后的章节中会详细介绍。绘制球体。
(5)onSurfaceChanged方法详细讲解。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MySurfaceView.java。
1 public void onSurfaceChanged(GL10 gl, int width, int height) { 2 //设置视窗大小及位置 3 gl.glViewport(0, 0, width, height); 4 //设置当前矩阵为投影矩阵 5 gl.glMatrixMode(GL10.GL_PROJECTION); 6 //设置当前矩阵为单位矩阵 7 gl.glLoadIdentity(); 8 //计算透视投影的比例 9 float ratio = (float) width / height; 10 //调用此方法计算产生透视投影矩阵 11 gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); 12 }
提示:第2~7行,onSurfaceChanged是在屏幕改变时调用的,该段代码的作用是设置视窗大小及位置,设置当前矩阵为投影矩阵,同时设置当前矩阵为单位矩阵。第8~11行,计算透视投影的比例、计算产生透视投影矩阵,同时设置投影模式为透视投影。
(6)onSurfaceCreated方法详细讲解。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MySurfaceView.java。
1 public void onSurfaceCreated(GL10 gl, EGLConfig config) { 2 //关闭抗抖动 3 gl.glDisable(GL10.GL_DITHER); 4 //打开抗锯齿 5 gl.glEnable(GL10.GL_MULTISAMPLE); 6 //设置特定Hint项目的模式,这里设置为使用快速模式 7 gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,GL10.GL_FASTEST); 8 //设置屏幕背景色黑色RGBA 9 gl.glClearColor(0,0,0,0); 10 //设置着色模型为平滑着色 11 gl.glShadeModel(GL10.GL_SMOOTH);//GL10.GL_SMOOTH GL10.GL_FLAT 12 //启用深度测试 13 gl.glEnable(GL10.GL_DEPTH_TEST); 14 gl.glEnable(GL10.GL_LIGHTING); //允许光照 15 initGreenLight(gl); //初始化绿色灯 16 initRedLight(gl); //初始化红色灯 17 initMaterial(gl); //初始化材质 18 }
● 第2~7行,关闭抗抖动、打开抗锯齿,同时设置特定Hint项目的模式,这里设置为使用快速模式。
● 第9~11行,设置屏幕背景色黑色RGBA、设置着色模型为平滑着色。
● 第13~17行,启用深度测试、允许光照,同时初始化绿色灯、初始化红色灯,以及初始化材质。
(7)初始化0号灯及1号灯,因为这两段代码较为相似,因此只给出初始化0号灯的代码。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MySurfaceView.java。
1 private void initGreenLight(GL10 gl){ 2 gl.glEnable(GL10.GL_LIGHT0); //打开0号灯 3 //环境光设置 4 float[] ambientParams={0.1f,0.1f,0.1f,1.0f}; //光参数RGBA 5 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientParams,0); 6 //散射光设置 7 float[] diffuseParams={0f,1f,1f,1.0f}; //光参数RGBA 8 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffuseParams,0); 9 //反射光设置 10 float[] specularParams={0.0f,1.0f,0.0f,1.0f}; //光参数RGBA 11 gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specularParams,0); 12 }
● 第2~5行,打开0号灯,同时设置0号灯的环境光属性。新建环境光float数组,环境光一般设置为较低。本例中材质为白色,所以RGB颜色设置相同。之后将环境光数组添加进入glLightfv方法的params参数。设置light参数为GL10.GL_LIGHT0,设置pname参数为GL10.GL_AMBIENT,设置offset为0。
● 第7~8行,设置0号灯的散射光属性。散射光一般设置为比环境光高,比反射光低。本例中材质为白色,所以RGB颜色设置相同。之后将散射光数组添加进入glLightfv方法的params参数。设置light参数为GL10.GL_LIGHT0,设置pname参数为GL10.GL_ DIFFUSE,设置offset为0。
● 第10~11行,设置0号灯的镜面光属性。镜面光一般设置较高。本例中材质为白色,所以RGB颜色设置相同。之后将镜面光数组添加进入glLightfv方法的params参数。设置light参数为GL10.GL_LIGHT0,设置pname参数为GL10.GL_ SPECULAR,设置offset为0。
(8)初始化材质相关代码讲解。
代码位置:本书随书光盘中源代码\第5章\Sample5_4\src\wyf\zcl\MySurfaceView.java。
1 private void initMaterial(GL10 gl){//材质为白色时什么颜色的光照在上面就将体现出什么颜色 2 //环境光为白色材质 3 float ambientMaterial[] = {0.4f, 0.4f, 0.4f, 1.0f}; 4 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, ambientMaterial,0); 5 //散射光为白色材质 6 float diffuseMaterial[] = {0.8f, 0.8f, 0.8f, 1.0f}; 7 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, diffuseMaterial,0); 8 //高光材质为白色 9 float specularMaterial[] = {1.0f, 1.0f, 1.0f, 1.0f}; 10 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, specularMaterial,0); 11 //高光反射区域,数越大,高亮区域越小、越暗 12 float shininessMaterial[] = {1.5f}; 13 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, shininessMaterial,0); 14 //蓝色自发光 15 float emission[] = {0.0f, 0.0f, 0.3f, 1.0f}; 16 gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION, emission,0); 17 }
● 第3~4行,新建环境光float数组,环境光一般设置为较低。本例中材质为白色,所以RGB颜色设置相同。之后将环境光数组添加进入glMaterialfv方法的params参数。设置face参数为GL_FRONT_AND_BACK,设置pname参数为GL10.GL_AMBIENT,设置offset为0。
● 第6~7行,新建散射光float数组,散射光一般设置为比环境光高,比反射光低。本例中材质为白色,所以RGB颜色设置相同。之后将散射光数组添加进入glMaterialfv方法的params参数。设置face参数为GL_FRONT_AND_BACK,设置pname参数为GL10.GL_DIFFUSE,设置offset为0。
本程序最终效果如图5-8所示。
图5-8(a) 红光转到球体后面的效果
图5-8(b) 3种光线的叠加效果
● 第9~10行,新建镜面光float数组,镜面光一般设置较高。本例中材质为白色,所以RGB颜色设置相同。之后将散射光数组添加进入glMaterialfv方法的params参数。设置face参数为GL_FRONT_AND_BACK,设置pname参数为GL10.GL_SPECULAR,设置offset为0。
● 第12~13行,新建高光反射区域float数组,高光反射区域数越大,高亮区域越小、越暗。之后将散射光数组添加进入glMaterialfv方法的params参数。设置face参数为GL_FRONT_AND_BACK,设置pname参数为GL10.GL_ SHININESS,设置offset为0。
● 第15~16行,本段代码设置球体为自发光,且为蓝色,通过glMaterialfv()函数可以进行设置,只需将其pname参数设置为GL10.GL_EMISSION即可。
5.5 面法向量与点平均法向量
在5.3节中介绍了法向量的作用,加上法向量的球体在光照下的反射效果就十分真实了。法向量分为面法向量和点法向量,这两种向量对于光的发射效果是不一样的。在本节将详细介绍两者之间的区别。
5.5.1 面法向量的应用
面法向量是指当立体物体中一个顶点位于某个三角形面上时,为其分配属于面的法向量。但很多情况下一个顶点并不只属于一个三角形面,而可能属于多个三角形面,如图5-9所示。而在OpenGL ES中不能给一个顶点分配多个法向量,因此一般采用的处理方法是此处的顶点属于几个面就在此处放置几个位置相同的顶点,并为这几个顶点分别指定属于面的法向量。
图5-9 一个顶点属于多个三角形面
由于采用面法向量时同一位置有多个顶点,每个顶点单独服务于某个三角形面,因此一般在使用面法向量时都是采用顶点法进行绘制的。
下面给出一个使用面法向量的例子Sample5_5,其运行效果如图5-10所示。
图5-10 面法向量案例运行效果图
说明: 上图中在立方体的每个顶点处实际上放置了多个位置相同的顶点,每个顶点都指定了其隶属的面法向量,因此开启光照效果后棱角分明,真实感很好。
了解该案例的原理后,下一步要做的就是通过实际开发来实现如图5-9所示的效果。下面就请读者跟随笔者思路进入该案例实现的阶段,其主要代码如下。
代码位置:本书随书光盘中源代码\第5章\Sample5_5\src\wyf\sj\CubeVertex.java。
1 float[] normals={ 2 //顶面顶点法向量 3 0,1,0,0,1,0,0,1,0, 4 0,1,0,0,1,0,0,1,0, 5 //后面顶点法向量 6 0,0,-1,0,0,-1,0,0,-1, 7 0,0,-1,0,0,-1,0,0,-1, 8 //前面顶点法向量 9 0,0,1,0,0,1,0,0,1, 10 0,0,1,0,0,1,0,0,1, 11 //下面顶点法向量 12 0,-1,0,0,-1,0,0,-1,0, 13 0,-1,0,0,-1,0,0,-1,0, 14 //左面顶点法向量 15 -1,0,0,-1,0,0,-1,0,0, 16 -1,0,0,-1,0,0,-1,0,0, 17 //右面顶点法向量 18 1,0,0,1,0,0,1,0,0, 19 1,0,0,1,0,0,1,0,0 20 }; 21 ByteBuffer nbb=ByteBuffer.allocateDirect(normals.length*4); //创建法向量坐标数据缓冲 22 nbb.order(ByteOrder.nativeOrder()); //设置字节顺序 23 mNormalBuffer=nbb.asFloatBuffer(); //转换为float型缓冲 24 mNormalBuffer.put(normals); //向缓冲区中放入顶点坐标数据 25 mNormalBuffer.position(0); //设置缓冲区起始位置 26 } 27 public void drawSelf(GL10 gl){ 28 ……//该处省略了部分不重要代码,读者可自行查看随书光盘中的源代码 29 gl.glEnableClientState(GL10.GL_NORMAL_ARRAY); //允许使用法向量数组 30 gl.glNormalPointer(GL10.GL_FLOAT, 0, mNormalBuffer); //为画笔指定顶点法向量数据 31 ……//该处省略了部分不重要代码,读者可自行查看随书光盘中的源代码 32 }
● 第1~20行主要负责为各顶点分配法向量坐标。
● 第21~26行为创建法向量坐标数据缓冲。
● 第29~31行为允许使用法向量数组,并为画笔指定顶点法向量数据。
提示:在绘制立方体时首先应该允许光照并且打开一个光源,本案例打开的为0号光源。
5.5.2 点平均法向量的应用
还有另外一种法向量,可以称之为点平均法向量,其含义为当同一个点隶属于多个面时,为其分配的是隶属的各个面法向量的平均值。如立方体中每个顶点属于3个面,因此每个顶点的法向量就是3个面法向量的平均值。
下面也给出了一个使用点平均法向量的案例Sample5_6,其运行效果如图5-11所示。
图5-11 点平均法向量效果图
提示:读者可以与图5-10进行比较,图片角度是一一对应的,可以很容易发现区别。面法向量棱角分明,而点平均法向量棱角柔和。
实现上述效果十分简单,只需要在为顶点分配法向量时计算出该点的平均法向量即可,其主要代码如下。
代码位置:本书随书光盘中源代码\第5章\Sample5_6\src\wyf\sj\CubeIndex.java。
1 float[] normals={ 2 -1,1,-1,1,1,-1, //为立方体顶面4个顶点分配平均法向量 3 -1,1,1,1,1,1, 4 -1,-1,-1,1,-1-1, //为立方体底面4个顶点分配平均法向量 5 -1,-1,1,1,-1,1 };
提示:该立方体的实现运用的是索引法,立方体共有8个顶点。为每个顶点分配的都是点平均法向量,因此法向量也只有8个。调用的绘制方法为gl.glDrawElements(GL10. GL_TRIANGLES, iCount, GL10.GL_UNSIGNED_BYTE, mIndexBuffer),请读者注意。由于篇幅有限,详细代码请读者自行查阅随书光盘,这里就不再一一赘述了。
从两个案例运行效果的比较中可以看出,面法向量和点平均法向量各有其不同的使用场合。
● 面法向量棱角分明,更适合实现本身就棱角分明的三维物体,如立方体,四棱锥等。
● 点平均法向量过渡平滑,比较适合表面平滑、没有棱角的物体,如茶壶、圆柱面、球等。
要特别注意的是,读者朋友千万不要认为有了点平均法向量的策略后,面法向量策略就不要了。如同上面所说的那样,使用哪种策略需要开发人员根据实际情况灵活掌握。
提示:在后面章节中将继续介绍两种不同的法向量带来的区别。
5.6 本章小结
关于光照的基本知识,都已经介绍给读者了。聪明的读者可能已经发现,光照这节如果融会贯通之后,其实没有多少内容,就是几个函数来回调用。对于这部分内容,理解原理,比如环境光、散射光及镜面光的不同,要比会使用这些函数更加重要。
相信大多数读者学习本书的目的都是要学以致用,那么理论和实际的结合就更加重要了。如果读者仅仅学会了如何使用各个函数,但是却不知道在什么情况下使用相应的函数,那么学习也就变得没有意义了。