本节书摘来自异步社区《OpenGL ES 2.0游戏开发(上卷):基础技术和典型案例》一书中的第6章,第6.2节基本光照效果,作者 吴亚峰,更多章节内容可以访问云栖社区“异步社区”公众号查看
6.2 基本光照效果
OpenGL ES 2.0游戏开发(上卷):基础技术和典型案例
完成了上一节中球体的开发后,下面的部分将基于此球体逐步介绍光照各个方面的知识。具体计划为首先介绍OpenGL ES 2.0中光照模型的基本知识,然后逐步为球体添加不同的光照效果。
6.2.1 光照的基本模型
如果要用一个数学模型完全真实地描述现实世界中的光照是很难的,一方面数学模型本身可能太过复杂;另一方面复杂的模型可能导致巨大的计算量。因此OpenGL ES 2.0中采用的光照模型相对现实世界进行了很大的简化,将光照分成了3种组成元素(也可以称为3个通道),包括环境光、散射光以及镜面光,具体情况如图6-5所示。
实际开发中,3个光照通道是分别采用不同的数学模型独立计算的,下面的几个小节将一一进行详细介绍。
6.2.2 环境光
环境光(Ambient)指的是从四面八方照射到物体上,全方位360都均匀的光。其代表的是现实世界中从光源射出,经过多次反射后,各方向基本均匀的光。环境光最大的特点是不依赖于光源的位置,而且没有方向性,图6-6简单地说明了这个问题。
图6-7界面上部的滑块用来控制光源的位置,由于环境光的照射效果与光源位置无关,故无论光源调节到什么位置效果都相同。另外,实际开发中环境光强度一般都设置得较弱,因此仅用环境光照射的物体看起来并不是很清楚。
了解了案例的运行效果后,就可以进行代码的开发了。由于本案例主要是对上一节中Sample6_1的升级,因此这里仅给出变化较大且有代表性的部分,具体如下所列。
(1)首先需要修改的是MySurfaceView类中的onDrawFrame方法,具体代码如下。
1 public void onDrawFrame(GL10 gl) {
2 GLES20.glClear( //清除深度缓冲与颜色缓冲
3 GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
4 MatrixState.pushMatrix(); //保护现场
5 MatrixState.pushMatrix();
6 MatrixState.translate(-1.2f, 0, 0); //沿_x_轴负方向平移
7 ball.drawSelf(); //绘制球
8 MatrixState.popMatrix();
9 MatrixState.pushMatrix();
10 MatrixState.translate(1.2f, 0, 0); //沿_x_轴正方向平移
11 ball.drawSelf(); //绘制球
12 MatrixState.popMatrix();
13 MatrixState.popMatrix(); //恢复现场
14 }
从上述代码中可以看出,此方法主要的变化是将原来只绘制一次球体更改为在左右两侧各绘制一次球体。绘制两次球体的目的是为后面开发其他光照效果服务,这里读者不必深究。
(2)在MySurfaceView类中添加了一个成员变量lightOffset,用来表示光源的x轴坐标。同时在Activity中为滑块添加了监听器,其功能为当滑块被拖动时修改lightOffset的值。
由于环境光与光源位置无关,故在本案例中拖拉滑块时场景没有变化,但后面开发其他光照效果时就很有作用了。
(3)完成了Java代码的修改后,接着就可以开发着色器了。首先是顶点着色器,其代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_2/assets目录下的vertex.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵
2 attribute vec3 aPosition; //顶点位置
3 varying vec3 vPosition; //用于传递给片元着色器的顶点位置
4 varying vec4 vAmbient; //用于传递给片元着色器的环境光分量
5 void main() {
6 gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点的位置
7 vPosition = aPosition; //将顶点的位置传递给片元着色器
8 vAmbient = vec4(0.15,0.15,0.15,1.0); //将环境光强度传递给片元着色器
9 }
上述代码中主要是增加了将环境光强度传递给片元着色器的代码。由于本案例比较简单,环境光的强度就固化在顶点着色器中了,未来有需要可以改为由Java程序传给顶点或片元着色器中的相关一致变量。
(4)开发完顶点着色器后,接下来就可以开发片元着色器了,其具体代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_2/assets目录下的frag.sh。
1 precision mediump float; //给出浮点精度
2 uniform float uR; //球半径
3 varying vec3 vPosition; //接收从顶点着色器过来的顶点位置
4 varying vec4 vAmbient; //接收从顶点着色器过来的环境光强度
5 void main(){
6 vec3 color;
7 ……//此处省略了按照棋盘着色器规则计算片元颜色值的代码,与前面Sample6_1
8 //案例中的完全相同,需要的读者请自行查看随书光盘中的源代码
9 vec4 finalColor=vec4(color,0); //最终颜色
10 gl_FragColor=finalColor*vAmbient;//根据环境光强度计算最终片元颜色值
11 }
上述片元着色器代码与前面案例的基本相同,主要是增加了接收环境光强度以及使用环境光强度与片元本身颜色值加权计算产生最终片元颜色值的相关代码。
6.2.3 散射光
上一小节中给出了仅仅使用环境光进行照射的案例,读者可能觉得效果并不好。确实如此,仅仅有环境光的场景效果是很差的,没有层次感。本节将介绍另外一种真实感好很多的光照效果——散射光(Diffuse),其指的是从物体表面向全方位360均匀反射的光,如图6-8所示。
散射光具体代表的是现实世界中粗糙的物体表面被光照射时,反射光在各个方向基本均匀(也称为“漫反射”)的情况,图6-9很好地说明了这个问题。
虽然反射后的散射光在各个方向是均匀的,但散射光反射的强度与入射光的强度以及入射的角度密切相关。因此当光源的位置发生变化时,散射光的效果会发生明显变化。主要体现为当光垂直地照射到物体表面时比斜照时要亮,其具体计算公式如下。
散射光照射结果=材质的反射系数×散射光强度×max(cos(入射角),0)
实际开发中往往分两步进行计算,此时公式被拆解为如下情况。
散射光最终强度=散射光强度×max(cos(入射角),0)
散射光照射结果=材质的反射系数×散射光最终强度
提示
材质的反射系数实际指的就是物体被照射处的颜色,散射光强度指的是散射光中RGB(红、绿、蓝)3个色彩通道的强度。
从上述公式中可以看出,与环境光计算公式唯一的区别是引入了最后一项“max(cos(入射角),0)”。其含义是入射角越大,反射强度越弱,当入射角的余弦值为负时(即入射角大于90°),反射强度为0。由于入射角为入射光向量与法向量的夹角,因此其余弦值并不需要调用三角函数进行计算,只需要首先将两个向量进行规格化,然后再进行点积即可,图6-10说明了这个问题。
图6-10中的N代表被照射点表面的法向量,P为被照射点,L为从P点到光源的向量。N与L的夹角即为入射角。向量数学中,两个向量的点积为两个向量夹角的余弦值乘以两个向量的模,而规格化后向量的模为1。因此,首先将两个向量规格化,再点积就可以求得两个向量夹角的余弦值。
提示
由于本书篇幅有限,关于向量数学的相关问题不作详细讨论,有兴趣的读者可以参考其他相关数学资料或书籍。
了解了散射光的基本原理后,下面给出一个使用了散射光的案例Sample6_3,其运行效果如图6-11所示。
图6-11左侧的图表示光源位于场景左侧进行照射的情况,右侧的图表示光源位于右侧进行照射的情况。从左右两幅效果图的对比中可以看出,正对光源(入射角小)的位置看起来较亮,而随着入射角的增大越来越暗,直至入射角大于90后完全不能照亮。
了解了散射光的基本原理及案例的运行效果后,就可以进行案例的开发了。由于实际上本案例仅仅是将案例Sample6_2复制了一份并进行了修改,因此这里仅给出修改的主要步骤,具体如下所列。
(1)由于散射光效果与光源的位置密切相关,因此需要将光源的位置传递进着色器以进行光照的计算。为了方便起见,首先需要对工具类MatrixState进行升级,增加存储当前光源位置的相关成员变量以及设置光源位置的方法,具体代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_3/com/bn/Sample6_3目录下的MatrixState.java。
1 public static float[] lightLocation = new float[] { 0, 0, 0 };//光源位置数组
2 public static FloatBuffer lightPositionFB; //光源位置的缓冲
3 static ByteBuffer llbbL = ByteBuffer.allocateDirect(3 * 4); //待用的字节缓冲
4 public static void setLightLocation(float x, float y, float z){//设置光源位置的方法
5 llbbL.clear(); //清除缓冲中原有的数据
6 lightLocation[0] = x;lightLocation[1] = y;lightLocation[2] = z; //将新的光源位置存入数组
7 llbbL.order(ByteOrder.nativeOrder()); //设置字节顺序
8 lightPositionFB = llbbL.asFloatBuffer(); //转换为float型缓冲
9 lightPositionFB.put(lightLocation); //将光源位置放入缓冲
10 lightPositionFB.position(0); //设置缓冲区起始位置
11 }
第1-3行为该类中新增加的成员变量,主要有光源位置数组、光源位置缓冲和待用字节缓冲。
第4-11行为设置光源位置的方法,该方法主要功能为将参数中传递过来的光源位置首先存入数组,然后再存放进对应的float型缓冲供渲染时传入管线。
(2)接着需要修改的是Ball类,主要是增加初始化法向量数据以及将法向量数据传入渲染管线的相关代码,具体情况如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_3/com/bn/Sample6_3目录下的Ball.java。
1 public class Ball {
2 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
3 int maNormalHandle; //顶点法向量属性引用
4 int maLightLocationHandle; //光源位置属性引用
5 FloatBuffer mNormalBuffer; //顶点法向量数据缓冲引用
6 public Ball(MySurfaceView mv) {/*代码省略*/}
7 public void initVertexData() { //初始化顶点数据的方法
8 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
9 ByteBuffer nbb = ByteBuffer.allocateDirect(vertices.length*4);//创建顶点法向量缓冲
10 nbb.order(ByteOrder.nativeOrder()); //设置字节顺序
11 mNormalBuffer = nbb.asFloatBuffer(); //转换为float型缓冲
12 mNormalBuffer.put(vertices); //向缓冲区中放入顶点法向量数据
13 mNormalBuffer.position(0); //设置缓冲区起始位置
14 }
15 public void initShader(MySurfaceView mv) { //初始化着色器
16 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
17 maNormalHandle= GLES20.glGetAttribLocation( //获取顶点法向量属性变量引用
18 mProgram, "aNormal");
19 maLightLocationHandle=GLES20.glGetUniformLocation(
20 mProgram, "uLightLocation"); //获取光源位置一致变量引用
21 }
22 public void drawSelf() {
23 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
24 GLES20.glUniform3fv( //将光源位置传入渲染管线
25 maLightLocationHandle, 1, MatrixState.lightPositionFB);
26 GLES20.glVertexAttribPointer( //将顶点位置数据传入渲染管线
27 maPositionHandle, 3, GLES20.GL_FLOAT,false, 3 * 4, mVertexBuffer);
28 GLES20.glVertexAttribPointer( //将顶点法向量数据传入渲染管线
29 maNormalHandle, 3, GLES20.GL_FLOAT, false,3 * 4, mNormalBuffer);
30 GLES20.glEnableVertexAttribArray(maPositionHandle); //启用顶点位置数据
31 GLES20.glEnableVertexAttribArray(maNormalHandle); //启用顶点法向量数据
32 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount);//绘制球体
33 }}
第3-5行增加了声明顶点法向量属性变量引用、光源位置一致变量引用及顶点法向量数据缓冲的代码。
第7-14行为初始化顶点数据的方法,其中增加了初始化顶点法向量数据缓冲的相关代码。由于本案例中原始情况下的球心位于坐标原点,所以每个顶点法向量的x、y、z轴分量与顶点的x、y、z坐标是一致的。这样就不必单独计算每个顶点的法向量了,直接将顶点坐标序列看作顶点法向量序列使用即可。
第15-21行在初始化着色器的方法中增加了获取顶点法向量属性变量引用以及光源位置一致变量引用的代码。
第22-33行为绘制球的drawSelf方法,其中增加了将法向量数据与光源位置数据传送进渲染管线的代码,同时也增加了启用顶点法向量数据的代码。
提示
并不是所有的情况下顶点法向量与顶点坐标都有必然联系,很多情况下顶点的法向量需要单独给出,本书中后面会有很多这样的案例。
(3)接下来需要修改的是MySurfaceView类中的onDrawFrame方法,具体代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_3/com/bn/Sample6_3目录下的MySurfaceView.java。
1 public void onDrawFrame(GL10 gl) {
2 GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT //清除深度缓冲与颜色缓冲
3 | GLES20.GL_COLOR_BUFFER_BIT);
4 MatrixState.setLightLocation(lightOffset, 0, 1.5f);//设置光源位置
5 MatrixState.pushMatrix(); //保护现场
6 ……//此处省略了绘制两次球的代码,请读者自行查看随书光盘中的源代码
7 MatrixState.popMatrix(); //恢复现场
8 }
提示
此方法变化不大,仅仅是在绘制每帧画面之前增加了设置光源位置的代码。这样当用户拖拉滑块改变光源的x坐标(lightOffset)时,绘制的画面就会随光源位置的变化而变化了。
(4)完成了MySurfaceView类的修改后,对Java代码的修改就基本完成了,下面需要修改的是着色器的代码。首先是顶点着色器,其代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_3/assets目录下的vertex.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵
2 uniform mat4 uMMatrix; //变换矩阵(包括平移、旋转、缩放)
3 uniform vec3 uLightLocation; //光源位置
4 attribute vec3 aPosition; //顶点位置
5 attribute vec3 aNormal; //顶点法向量
6 varying vec3 vPosition; //用于传递给片元着色器的顶点位置
7 varying vec4 vDiffuse; //用于传递给片元着色器的散射光分量
8 void pointLight ( //散射光光照计算的方法
9 in vec3 normal, //法向量
10 inout vec4 diffuse, //散射光计算结果
11 in vec3 lightLocation, //光源位置
12 in vec4 lightDiffuse //散射光强度
13 ){
14 vec3 normalTarget=aPosition+normal; //计算变换后的法向量
15 vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz;
16 newNormal=normalize(newNormal); //对法向量规格化
17 //计算从表面点到光源位置的向量vp
18 vec3 vp= normalize(lightLocation-(uMMatrix*vec4(aPosition,1)).xyz);
19 vp=normalize(vp); //规格化vp
20 float nDotViewPosition=max(0.0,dot(newNormal,vp)); //求法向量与vp向量的点积与0的最大值
21 diffuse=lightDiffuse*nDotViewPosition; //计算散射光的最终强度
22 }
23 void main(){
24 gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点的位置
25 vec4 diffuseTemp=vec4(0.0,0.0,0.0,0.0);
26 pointLight(normalize(aNormal), diffuseTemp, uLightLocation, vec4(0.8,0.8,0.8,1.0));
27 vDiffuse=diffuseTemp; //将散射光最终强度传给片元着色器
28 vPosition = aPosition; //将顶点的位置传给片元着色器
29 }
第1-7行为顶点着色器中全局变量的声明,主要是增加了光源位置一致变量uLightLocation、法向量属性变量aNorma、以及用于传递给片元着色器的散射光最终强度易变变量vDiffuse。
第8-22行为根据前面介绍的公式计算散射光最终强度的pointLight方法,最重要的是在进行计算前要对顶点法向量进行变换,将法向量变换到当前的姿态下。
第23-29行为顶点着色器的main方法,其中首先增加了调用pointLight方法计算散射光最终强度的代码,同时也增加了将计算出的散射光最终强度值传递给片元着色器的代码。
(5)完成了顶点着色器的开发后,就可以开发片元着色器了,其代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_3/assets目录下的frag.sh。
1 precision mediump float; //给出默认浮点精度
2 uniform float uR; //球的半径
3 varying vec3 vPosition; //接收从顶点着色器传递过来的顶点位置
4 varying vec4 vDiffuse; //接收从顶点着色器传递过来的散射光最终强度
5 void main() {
6 vec3 color;
7 ……//此处省略了计算片元颜色值的代码,与前面案例中的相同
8 ……//请读者自行查看随书光盘中的源代码
9 vec4 finalColor=vec4(color,0); //最终颜色
10 gl_FragColor=finalColor*vDiffuse; //根据散射光最终强度计算片元的最终颜色值
11 }
第4行增加了vDiffuse易变变量的声明,其功能为接收从顶点着色器传递过来的散射光最终强度。
第10行在将片元颜色值传递给渲染管线之前,增加了其与散射光最终强度加权计算的操作。
6.2.4 镜面光
使用了上一小节中介绍的散射光效果后,场景的整体效果有了较大的提升。但这并不是光照的全部,现实世界中当光滑表面被照射时会有方向很集中的反射光。这就是镜面光(Specular),本小节将详细介绍镜面光的计算模型。
与散射光最终强度仅依赖于入射光与被照射点法向量的夹角不同,镜面光的最终强度还依赖于观察者的位置。也就是说,如果从摄像机到被照射点的向量不在反射光方向集中的范围内,观察者将不会看到镜面光,图6-12简单地说明了这个问题。
镜面光的计算模型比前面的两种光都要复杂一些,具体公式如下。
镜面光照射结果=材质的反射系数×镜面光强度×max(0,(cos(半向量与法向量的夹角)) 粗糙度)
实际开发中往往分两步进行计算,此时公式被拆解为如下情况。
镜面光最终强度=镜面光强度×max(0,(cos(半向量与法向量的夹角)) 粗糙度)
镜面光照射结果=材质的反射系数×镜面光最终强度
提示
材质的反射系数实际指的就是物体被照射处的颜色,镜面光强度指的是镜面光中RGB(红、绿、蓝)3个色彩通道的强度。
从上述公式中可以看出,与散射光计算公式主要有两点区别。首先是计算余弦值时对应的角不再是入射角,而是半向量与法向量的夹角。半向量指的是从被照射点到光源的向量与从被照射点到观察点向量的平均向量,图6-13说明了半向量的含义。
图6-13中V为从被照射点到观察点的向量,N为被照射点表面的法向量,H为半向量,L为从被照射点到光源的向量。
从图6-13中可以看出,半向量H与V及L共面,并且其与这两个向量的夹角相等。因此已知V和L后计算H非常简单,只要首先将V和L规格化,然后将规格化后的V与L求和并再次规格化即可。求得半向量后,再求其与法向量夹角的余弦值就非常简单了,只需将规格化后的法向量与半向量进行点积即可。
另外一个区别就是求得的余弦值还需要对粗糙度进行乘方运算,此运算可以达到粗糙度越小,镜面光面积越大的效果,这也是很贴近现实世界的。
提示
由于本书篇幅有限,故仅仅介绍了镜面光计算公式本身,而没有深入讨论为什么会产生这样的公式,有兴趣的读者可以参考其他相关资料或书籍。
了解了镜面光的基本原理后,下面给出一个使用镜面光的案例Sample6_4,其运行效果如图6-14所示。
说明
图6-14中左侧为粗糙度值等于25的情况,右侧为粗糙度值等于50的情况。从左右两侧运行效果图的对比中可以看出,粗糙度越小,镜面光面积越大,这也符合我们观察现实世界的经验。另外从图中还可以看出,镜面光也是随光源位置的变化而变化的。
了解了镜面光的基本原理及案例的运行效果后,就可以进行案例的开发了。由于实际上本案例仅仅是将案例Sample6_3复制了一份并进行了修改,因此这里仅给出修改的主要步骤,具体如下所列。
(1)由于镜面光的计算不仅与光源位置有关,还与摄像机位置有关,故摄像机的位置也需要传入渲染管线。为了实现此目标,首先需要对MatrixState类中设置摄像机的方法进行升级并增加相关的成员变量,具体代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_4/com/bn/Sample6_4目录下的MatrixState.java。
1 static ByteBuffer llbb= ByteBuffer.allocateDirect(3*4);//待用的字节缓冲
2 static float[] cameraLocation=new float[3]; //摄像机位置
3 public static FloatBuffer cameraFB; //摄像机位置数据缓冲
4 public static void setCamera( //设置摄像机的方法
5 float cx, float cy, float cz, //摄像机位置的_X_、_Y_、_Z_坐标
6 float tx, float ty, float tz, //观察目标点_X_、_Y_、_Z_坐标
7 float upx, float upy, float upz //up向量在_X_、_Y_、_Z_轴上的分量
8 ) {
9 Matrix.setLookAtM(mVMatrix, 0, cx, cy, cz, tx, ty, tz, upx, upy, upz); //产生摄像机观察矩阵
10 cameraLocation[0]=cx;cameraLocation[1]=cy;cameraLocation[2]=cz; //将摄像机坐标记录进数组
11 llbb.clear(); //清除摄像机位置缓冲
12 llbb.order(ByteOrder.nativeOrder()); //设置字节顺序
13 cameraFB=llbb.asFloatBuffer(); //转换为float型缓冲
14 cameraFB.put(cameraLocation); //将摄像机位置放入缓冲
15 cameraFB.position(0); //设置缓冲区起始位置
16 }
第1-3行为该类中新增加的成员变量,主要有摄像机位置数组、摄像机位置缓冲和待用的字节缓冲。
第4-16行为设置摄像机位置的方法,主要是增加了将摄像机x、y、z坐标先存储进数组,然后再存放进对应的float型缓冲的相关代码。摄像机位置存放进缓冲后,绘制时就可以根据需要传送进渲染管线供着色器进行镜面光计算使用。
(2)接着需要修改的是Ball类,主要是增加了将摄像机位置传送进渲染管线的相关代码,具体内容如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_4/com/bn/Sample6_4目录下的Ball.java。
1 public class Ball {
2 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
3 int maCameraHandle; //摄像机位置属性引用
4 public Ball(MySurfaceView mv) {/*代码省略*/}
5 public void initVertexData() {/*代码省略*/}
6 public void initShader(MySurfaceView mv) { //初始化着色器
7 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
8 maCameraHandle=GLES20.glGetUniformLocation(//获取摄像机位置一致变量引用
9 mProgram, "uCamera");
10 }
11 public void drawSelf() {
12 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
13 GLES20.glUniform3fv( //将摄像机位置传入渲染管线
14 maCameraHandle, 1, MatrixState.cameraFB);
15 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
16 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount); //绘制球
17 }}
第3行增加了声明摄像机位置一致变量引用的代码。
第8-9行在初始化着色器的方法中增加了获取摄像机位置一致变量引用的代码。
第11-17行为绘制球的drawSelf方法,其中增加了将摄像机位置传送进渲染管线的代码。
(3)完成了上述修改后,对Java代码的修改就基本完成了,下面需要修改的是着色器的代码。首先是顶点着色器,其代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_4/assets目录下的vertex.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵
2 uniform mat4 uMMatrix; //变换矩阵
3 uniform vec3 uLightLocation; //光源位置
4 uniform vec3 uCamera; //摄像机位置
5 attribute vec3 aPosition; //顶点位置
6 attribute vec3 aNormal; //法向量
7 varying vec3 vPosition; //用于传递给片元着色器的顶点位置
8 varying vec4 vSpecular; //用于传递给片元着色器的镜面光最终强度
9 void pointLight( //定位光光照计算的方法
10 in vec3 normal, //法向量
11 inout vec4 specular, //镜面光最终强度
12 in vec3 lightLocation, //光源位置
13 in vec4 lightSpecular //镜面光强度
14 ){
15 vec3 normalTarget=aPosition+normal; //计算变换后的法向量
16 vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz;
17 newNormal=normalize(newNormal); //对法向量规格化
18 //计算从表面点到摄像机的向量
19 vec3 eye= normalize(uCamera-(uMMatrix*vec4(aPosition,1)).xyz);
20 //计算从表面点到光源位置的向量vp
21 vec3 vp= normalize(lightLocation-(uMMatrix*vec4(aPosition,1)).xyz);
22 vp=normalize(vp);//格式化vp
23 vec3 halfVector=normalize(vp+eye); //求视线与光线的半向量
24 float shininess=50.0; //粗糙度,越小越光滑
25 float nDotViewHalfVector=dot(newNormal,halfVector); //法线与半向量的点积
26 float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess));//镜面反射光强度因子
27 specular=lightSpecular*powerFactor; //最终的镜面光强度
28 }
29 void main() {
30 gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点的位置
31 vec4 specularTemp=vec4(0.0,0.0,0.0,0.0);
32 pointLight(normalize(aNormal), specularTemp, uLightLocation, vec4(0.7,0.7,0.7,
1.0)); //计算镜面光
33 vSpecular=specularTemp; //将最终镜面光强度传给片元着色器
34 vPosition = aPosition; //将顶点的位置传给片元着色器
35 }
第1-8行为顶点着色器中全局变量的声明,主要是增加了摄像机位置一致变量uCamera、用于传递给片元着色器的镜面光最终强度易变变量vSpecular。
第9-28行为根据前面介绍的公式计算镜面光最终强度的pointLight方法。
第29-35行为顶点着色器的main方法,其中首先调用pointLight方法计算镜面光的最终强度,并将计算出的镜面光最终强度值传递给片元着色器。
(4)完成了顶点着色器的开发后,就可以开发片元着色器了,其代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_4/assets目录下的frag.sh。
1 precision mediump float; //给出默认的浮点精度
2 uniform float uR;
3 varying vec3 vPosition; //接收从顶点着色器传递过来的顶点位置
4 varying vec4 vSpecular; //接收从顶点着色器传递过来的镜面光最终强度
5 void main(){
6 vec3 color;
7 ……//此处省略了计算片元颜色值的代码,请读者自行查看随书光盘中的源代码
8 vec4 finalColor=vec4(color,0); //最终颜色
9 gl_FragColor=finalColor*vDiffuse; //根据镜面光最终强度计算片元的最终颜色值
10 }
说明
上述片元着色器的代码与散射光的套路基本一致,只是将接收散射光最终强度并与片元颜色值加权计算换成了接收镜面光最终强度并与片元颜色值加权计算。
6.2.5 三种光照通道的合成
前面3个小节案例中的每个仅采用了一种光照通道,而现实世界中3种通道是同时作用的。因此本小节将通过一个案例Sample6_5将前面3个小节不同通道(环境光、散射光、镜面光)的光照效果综合起来,其运行效果如图6-15所示。
提示
从图6-15中可以看出,综合了3种光照通道后,场景的真实感大大增强。
了解了案例的运行效果后,就可以进行案例的开发了。由于实际上本案例仅仅是将案例Sample6_2、Sample6_3和Sample6_4中顶点着色器及片元着色器计算光照的相关代码进行了综合,因此这里仅给出综合后着色器的代码。
(1)首先综合了3种光照通道的顶点着色器,其具体代码如下。
第X问1 代码位置:见随书光盘中源代码/第6章/Sample6_5/assets目录下的vertex.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵
2 uniform mat4 uMMatrix; //变换矩阵
3 uniform vec3 uLightLocation; //光源位置
4 uniform vec3 uCamera; //摄像机位置
5 attribute vec3 aPosition; //顶点位置
6 attribute vec3 aNormal; //法向量
7 varying vec3 vPosition; //用于传递给片元着色器的顶点位置
8 varying vec4 vAmbient; //用于传递给片元着色器的环境光最终强度
9 varying vec4 vDiffuse; //用于传递给片元着色器的散射光最终强度
10 varying vec4 vSpecular; //用于传递给片元着色器的镜面光最终强度
11 void pointLight( //定位光光照计算的方法
12 in vec3 normal, //法向量
13 inout vec4 ambient, //环境光最终强度
14 inout vec4 diffuse, //散射光最终强度
15 inout vec4 specular, //镜面光最终强度
16 in vec3 lightLocation, //光源位置
17 in vec4 lightAmbient, //环境光强度
18 in vec4 lightDiffuse, //散射光强度
19 in vec4 lightSpecular //镜面光强度
20 ){
21 ambient=lightAmbient; //直接得出环境光的最终强度
22 vec3 normalTarget=aPosition+normal; //计算变换后的法向量
23 vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz;
24 newNormal=normalize(newNormal); //对法向量规格化
25 //计算从表面点到摄像机的向量
26 vec3 eye= normalize(uCamera-(uMMatrix*vec4(aPosition,1)).xyz);
27 //计算从表面点到光源位置的向量vp
28 vec3 vp= normalize(lightLocation-(uMMatrix*vec4(aPosition,1)).xyz);
29 vp=normalize(vp);//格式化vp
30 vec3 halfVector=normalize(vp+eye); //求视线与光线的半向量
31 float shininess=50.0; //粗糙度,越小越光滑
32 float nDotViewPosition=max(0.0,dot(newNormal,vp)); //求法向量与vp的点积与0的最大值
33 diffuse=lightDiffuse*nDotViewPosition; //计算散射光的最终强度
34 float nDotViewHalfVector=dot(newNormal,halfVector);//法线与半向量的点积
35 float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess));//镜面反射光强度因子
36 specular=lightSpecular*powerFactor; //计算镜面光的最终强度
37 }
38 void main(){
39 gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点位置
40 vec4 ambientTemp,diffuseTemp,specularTemp; //用来接收3个通道最终强度的变量
41 pointLight(normalize(aNormal),ambientTemp,diffuseTemp,specularTemp,uLightLocation,
42 vec4(0.15,0.15,0.15,1.0),vec4(0.8,0.8,0.8,1.0),vec4(0.7,0.7,0.7,1.0)); //计算定位光各通道强度
43 vAmbient=ambientTemp; //将环境光最终强度传给片元着色器
44 vDiffuse=diffuseTemp; //将散射光最终强度传给片元着色器
45 vSpecular=specularTemp; //将镜面光最终强度传给片元着色器
46 vPosition = aPosition; //将顶点的位置传给片元着色器
47 }
提示
上述代码只是将3种通道光照的计算都综合到了pointLight方法中,并将计算出来的3种通道光的最终强度都传递给了片元着色器。
(2)完成了顶点着色器的开发后,就可以开发片元着色器了,其代码如下。
1 precision mediump float; //指定默认浮点精度
2 uniform float uR; //球的半径
3 varying vec3 vPosition; //接收从顶点着色器传递过来的顶点位置
4 varying vec4 vAmbient; //接收从顶点着色器传递过来的环境光最终强度
5 varying vec4 vDiffuse; //接收从顶点着色器传递过来的散射光最终强度
6 varying vec4 vSpecular; //接收从顶点着色器传递过来的镜面反射光最终强度
7 void main(){
8 vec3 color;
9 ……//此处省略了计算片元颜色值的代码,请读者自行查看随书光盘中的源代码
10 vec4 finalColor=vec4(color,0); //最终颜色
11 //综合3个通道光的最终强度及片元的颜色计算出最终片元的颜色并传递给管线
12 gl_FragColor=finalColor*vAmbient + finalColor*vDiffuse + finalColor*vSpecular;
13 }
提示
上述代码将原来单独计算的各个光照通道综合在一起计算,产生同时受3个通道影响的最终片元颜色值后再将其传递给渲染管线。
前面的案例中都是将光各个通道的原始强度固化在着色器程序中的,如果有需要,读者可以自行开发出将光各个通道的强度由宿主程序传入着色器一致变量的版本。另外前面的案例中也仅有一个光源,如果有需要,读者可以根据前面案例介绍的内容开发出多个光源叠加照射的效果。
还需要注意的是,本节介绍的光照计算模型是比较常用的也是比较简单的一套,还有很多其他更为复杂的可以取得更好效果的光照计算模型,读者有需要也可以进一步参考其他技术资料。