百纳科技 审校
北京
图书在版编目(CIP)数据
OpenGL ES 2.0游戏开发.上卷,基础技术和典型案例/吴亚峰著.--北京:人民邮电出版社,2014.3
ISBN 978-7-115-33916-4
Ⅰ.①O… Ⅱ.①吴… Ⅲ.①图形软件—软件开发 Ⅳ.①TP391.41
中国版本图书馆CIP数据核字(2013)第294923号
内容提要
本书共分为17 章,内容按照必知必会的基础知识、基于 OpenGL ES 2.0 实现基本特效以及真实大型游戏案例的顺序进行详细地讲解。主要内容如下:OpenGL 的渲染管线、着色语言(Shading Language)、投影及各种变换、OpenGL ES 2.0 中光照的基本原理与实现、纹理映射的基本原理与使用、各种 3D 基本形状的原理与实现、3D模型加载、混合及雾的基本原理与使用、3D开发技巧(包括标志版、灰度图地形、粒子沉积地形、天空盒与天空穹、镜像技术、动态文本输出以及非真实感绘制等一些常用的 3D开发技巧)、剪裁与测试、Android 中各种传感器的使用,以及 NDK 及iOS 平台下的OpenGL ES 开发,同时还介绍了如何在不同的平台间进行 3D 应用的移植;WebGL 3D 应用开发,如何将 3D 应用移植到WebGL 平台。最后用两大完整案例(就是夜鹰行动和 BN 赛艇)贯穿整本书的实战知识,真正帮助读者学以致用。
本书中在给出实际的开发案例时涉及了 Android SDK、Android NDK、iOS、WebGL 等主流平台,书中关于可编程渲染管线着色器的知识与技术还可以通用于包括 Windows Phone、MeeGO、PSP、PlayStation等移动嵌入式平台。
本书适合初学者、移动开发者、游戏开发者、程序员学习,也适合大中专院校的相关专业的师生用书和培训学校的教材。
◆著 吴亚峰
审校 百纳科技
责任编辑 张涛
责任印制 程彦红 焦志炜
◆人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
北京鑫正大印刷有限公司印刷
◆开本:787×1092 1/16
印张:30 彩插:2
字数:829千字 2014年3月第1版
印数:1-3500册 2014年3月北京第1次印刷
定价:79.00元(附光盘)
读者服务热线:(010)81055410 印装质量热线:(010)81055316
反盗版热线:(010)81055315
广告经营许可证:京崇工商广字第0021号
为什么要写这样的一本书
随着智能手机硬件性能的不断提升,如水果忍者、极品飞车、会说话的Tom猫等一批优秀的3D游戏娱乐应用在广大智能机用户间流行开来,与此同时,也带动了手机游戏产业逐渐从2D走向3D。但目前国内专门介绍这方面开发的书籍与资料都非常少,同时3D应用开发的门槛又比较高,使得很多初学者无从下手。根据这种情况,笔者结合多年从事3D游戏应用开发的经验编写了本书。
了解一些3D领域的技术人员都知道,移动嵌入式平台上的3D应用开发主要是基于OpenGL ES的3 个版本,即OpenGL ES 1.0、1.1和2.0。OpenGL ES 1.0 与1.1 是较老的版本,渲染能力很有限,留给开发人员发挥的空间也很有限。本书主要介绍了OpenGL ES 2.0,其采用的是可编程渲染管线,留给了开发人员充分的发挥空间。
本书中在给出实际的开发案例时涉及了Android SDK、Android NDK、iOS、WebGL 等主流平台,充分考虑到了各个不同主流目标平台读者的需求。同时,本书中关于OpenGL ES 2.0 可编程渲染管线着色器的知识与技术还可以通用于其他的很多移动嵌入式平台,包括Windows Phone、MeeGO、PSP、PlayStation等。因此,学习各种移动嵌入式平台下高级3D应用开发的读者都可以参考此书。
经过一年多见缝插针式的写作,本书终于交稿了。回顾写书的这一年时间,不禁为自己能最终完成这个耗时费力的“大制作”而感到欣慰。同时也为自己能将从事游戏开发近10年来积累的宝贵经验以及编程感悟分享给正在开发阵线上埋头苦干的广大开发人员而感到高兴。
贾岛的《剑客》一诗有言:“十年磨一剑,霜刃未曾试,今日把示君,谁有不平事?”从1998年首次接触Java与OpenGL算起,到现在也是10年有余。笔者希望用10年的知识和经验磨出的利剑能够帮助广大读者在实际工作中披荆斩棘、奋勇向前。
本书特点
1.内容丰富,由浅入深
本书组织上本着“起点低,终点高”的原则,内容覆盖了从学习OpenGL ES 2.0 必知必会的基础知识到基于OpenGL ES 2.0实现各种高级特效,书的最后还给出了完整的大型3D游戏案例。其中的两个案例是笔者带领学员参加谷歌(Google)2011第二届Android应用开发中国大学生挑战赛的获奖作品,其中第16章的夜鹰行动更是获全国总决赛前5名大奖的作品。
这样的内容组织使学习移动嵌入式 3D 应用开发的初学者可以一步一步成长为 3D 开发的达人,符合绝大部分想学习3D应用开发的学生与程序开发人员以及相关技术人员的需求。
2.结构清晰,讲解到位
本书中配合每个需要讲解的知识点都给出了丰富的插图与完整的案例,使得初学者易于上手,有一定基础的读者便于深入。书中所有的案例均是根据笔者多年的开发心得进行设计的,结构清晰,便于读者进行学习。同时书中还给出了很多笔者多年来积累的编程技巧与心得,具有很高的参考价值。
3.非常实用的光盘
为了便于读者学习,本书附赠的光盘中包含了书中所有案例的完整源代码,最大限度地帮助读者快速掌握开发技术。
内容导读
本书共分为17 章,内容按照必知必会的基础知识、基于OpenGL ES 2.0实现基本特效以及真实大型游戏案例的顺序进行详细地讲解。
本书内容丰富,从基本知识到高级特效;从简单的应用程序到完整的 3D 游戏案例,适合不同需求、不同水平层次的各类读者。
● 初学OpenGL ES 2.0 3D 应用开发的读者
本套书《OpenGL ES 2.0游戏开发(上卷)》和《OpenGL ES 2.0游戏开发(下卷)》内容包括在各个主流平台下进行 3D 应用开发各方面的知识,内容由浅入深,配合详细的案例。非常适合3D游戏的初学者循序渐进地学习,最终成为3D游戏应用开发的达人。
● 有一定3D 开发基础希望进一步深入学习OpenGL ES 2.0 高级3D 开发技术的读者
本套书不仅包括了OpenGL ES 2.0 开发的基础知识,同时也包括了基于OpenGL ES 2.0 实现高级特效以及完整的游戏案例,有利于有一定基础的开发人员进一步提高开发水平与能力。
本书作者
吴亚峰,毕业于北京邮电大学,后留学澳大利亚卧龙岗大学取得硕士学位。1998年开始从事Java应用的开发,有10 多年的Java开发与培训经验。主要的研究方向为OpenGL ES、手机游戏、Java EE 以及搜索引擎。同时为手机游戏、Java EE独立软件开发工程师,并兼任百纳科技Java 培训中心首席培训师。近10年来为数10家著名企业培养了上千名高级软件开发人员,曾编写过《菜鸟成长之路——Java 程序员职场全攻略》、《Android 3D 游戏案例开发大全》、《Android 平板电脑开发实战详解和典型案例》、《Android游戏开发大全》、《Android应用案例开发大全》、《Unity 3D游戏开发技术详解与典型案例》等多本畅销技术书籍。2008年年初开始关注Android平台下的应用开发,并开发出一系列优秀的Android 3D 应用程序与游戏。
本书在编写过程中得到了唐山百纳科技有限公司Java培训中心的大力支持,同时仇磊、李腾飞、夏学良、王旅波、李胜杰、代其祥、蒋科、任俊钢、章雅卓、付鹏、白冰以及家人为本书的编写提供了很多帮助,在此表示衷心的感谢!
由于笔者的水平和学识有限,且书中涉及的知识较多,难免有错误和疏漏之处,敬请广大读者批评指正,并多提宝贵意见。本书责任编辑联系邮箱为:zhangtao@ptpress.com.cn。
编者
通过前面章节的学习,读者已经有能力基于OpenGL ES 2.0 开发出简单的3D 场景了。但对于场景中的物体只能通过直接给出颜色的方式进行着色渲染,真实感较差。本章将向读者介绍光照效果的开发,通过本章的学习,读者可以为场景中的物体增加逼真的光照效果,大大提升了场景的真实感。
前面的章节中已经介绍了如何构建3D物体,但案例中的3D物体基本都是平面性质的(直接的平面形状或长方体等),还没有曲面性质的物体。对于演示光照效果而言,曲面物体更能凸显出光照效果的强大。因此,在正式介绍光照之前,本节首先基于球体的构建向读者简单介绍一下曲面物体的构建策略。
通过上一章的学习读者已经知道,OpenGL ES中任何形状的3D 物体都是用三角形拼凑而成的,因此,构建曲面物体最重要的就是找到将曲面恰当拆分成三角形的策略。最基本的策略是首先按照一定的规则将物体按行和列两个方向进行拆分,这时就可以得到很多的小四边形。然后再将每个小四边形拆分成两个三角形即可,图6-1给出了基于这种策略的球面拆分思路。
从图6-1中可以看出,球面首先被按照纬度(行)和经度(列)的方向拆分成了很多的小四边形,每个小四边形又被拆分成两个小三角形。这种拆分方式下,三角形中每个顶点的坐标都可以用解析几何的公式方便地计算出来,具体情况如下。
x=R×cosα×cosβ; y=R×cosα×sinβ; z=R×sinα
上述给出的是当球的半径为R,在纬度为α,经度为β处球面上顶点坐标的计算公式。
提示
对一个曲面物体进行拆分时,以何为行,以何为列是不一定的,读者应该根据具体情况做出选择。
对曲面物体进行拆分时,拆分得越细,最终的绘制结果就越接近真实情况,图6-2很好地说明了这个问题。
提示
图6-2中从左至右依次为按照90°/份、45°/份、22.5°/份、11.25°/份对球面进行切分的情况,可以明显地看出,切分得越细,就越接近于真实的曲面。但也不是越细越好,切分得太细就会造成顶点数量过多,渲染速度大大降低。因此在开发中读者要掌握好两者之间的平衡,兼顾速度与效果。
上一小节已经介绍了如何将球面拆分成一组小三角形,下面就可以基于上一小节介绍的原理开发出球体绘制的案例Sample6_1了。正式开发代码之前,有必要首先了解一下本案例的运行效果,如图6-3所示。
从图6-3中可以看出,本案例中的球体不是用单一颜色进行着色的,采用的是棋盘纹理着色器。棋盘纹理着色器是一种非常简单的着色器,其原理如图6-4所示。
说明
图 6-4 中的立方体为球的外接立方体,球面上的每个位置都在此外接立方体之内,此外接立方体沿x、y、z轴方向被切分成了很多同样尺寸的小方块。
具体的着色策略为,若片元位于黑色小方块中,就将该片元的颜色设置为红色;若片元位于浅灰色小方块中则将片元的颜色设置为白色,具体计算方法如下所列。
● 首先计算出当前片元x、y、z 坐标对应的行数(x 轴)、层数(y 轴)及列数(z 轴)。
● 如果行数、层数、列数之和为奇数,则片元为红色;若和为偶数,则片元为白色。
了解了案例的运行效果与基本原理后,就可以进行代码的开发了,具体步骤如下。
提示
由于本案例中的很多类与前面章节案例中的很类似,因此在这里只给出本案例中具有特殊性及代表性的代码。若读者对省略的代码感兴趣,可以参考随书光盘。
(1)首先需要介绍的是负责按照切分规则生成球面上顶点的坐标,并渲染球体的Ball类,其代码框架如下。
代码位置:见随书光盘中源代码/第6章/Sample6_1/com/bn/Sample6_1目录下的Ball.java。
1 package com.bn.Sample6_1; //声明包
2 import java.nio.ByteBuffer; //引入相关类
3 ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4 import android.opengl.GLES20; //引入相关类
5 public class Ball { //球
6 int mProgram; //自定义渲染管线着色器程序id
7 int muMVPMatrixHandle; //总变换矩阵引用
8 int maPositionHandle; //顶点位置属性引用
9 int muRHandle; //球的半径参数引用
10 String mVertexShader; //顶点着色器代码脚本
11 String mFragmentShader; //片元着色器代码脚本
12 FloatBuffer mVertexBuffer; //顶点坐标数据缓冲
13 int vCount = 0; //顶点数量
14 float yAngle = 0;float xAngle = 0;float zAngle = 0; //绕x、y、Z轴旋转的角度
15 float r = 0.8f; //球的半径
16 public Ball(MySurfaceView mv) {
17 initVertexData(); //初始化顶点坐标
18 initShader(mv); //初始化着色器
19 }
20 public void initVertexData() {
21 ……//此处省略了初始化顶点坐标方法的代码,将在后面步骤中介绍
22 }
23 public void initShader(MySurfaceView mv) { //初始化着色器的方法
24 ……//此处省略了部分代码,读者可自行查看随书光盘的源代码
25 //获取着色器程序中球半径参数的引用
26 muRHandle = GLES20.glGetUniformLocation(mProgram, "uR");
27 }
28 public void drawSelf() {
29 ……//此处省略了部分代码,读者可自行查看随书光盘的源代码
30 GLES20.glUniform1f(muRHandle, r * UNIT_SIZE); //将半径属性传入渲染管线
31 GLES20.glVertexAttribPointer( //将顶点属性传入渲染管线
32 maPositionHandle, 3, GLES20.GL_FLOAT, false, 3 * 4, mVertexBuffer);
33 GLES20.glEnableVertexAttribArray(maPositionHandle);//允许渲染管线顶点位置数据
34 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount);//绘制球
35 }}
● 第 6-15 行声明了一些成员变量,其中大部分都与前面的很多案例类似。最大的区别是增添了着色器中球半径参数的引用muRHandle以及球的半径r。
● 第23-27 行为初始化着色器的initShader 方法,其中大部分代码与前面很多的案例类似,主要区别为增加了获取着色器程序中球半径参数引用的代码。
● 第28-35行为绘制球体的drawSelf 方法,与前面案例中此方法的主要区别是增加了将当前球的半径传入渲染管线的代码。
(2)接下来介绍上一步骤中省略的负责初始化顶点的initVertexData方法,其具体代码如下。
代码位置:见随书光盘中源代码/第6章/Sample6_1/com/bn/Sample6_1目录下的Ball.java。
1 public void initVertexData() {
2 ArrayList<Float> alVertix = new ArrayList<Float>();//存放顶点坐标值的ArrayList
3 final int angleSpan = 10; //将球进行单位切分的角度
4 for (int vAngle = -90; vAngle < 90; vAngle = vAngle + angleSpan) {//纬度方向angleSpan度一份
5 for (int hAngle = 0; hAngle <= 360; hAngle = hAngle + angleSpan){//经度方向angleSpan度一份
6 //计算出以当前经度、纬度位置的顶点为左上侧点的四边形4个顶点的坐标
7 float x0 = (float) (r * UNIT_SIZE //第1个顶点的坐标
8 * Math.cos(Math.toRadians(vAngle)) * Math.cos(Math
9 .toRadians(hAngle)));
10 float y0 = (float) (r * UNIT_SIZE
11 * Math.cos(Math.toRadians(vAngle)) * Math.sin(Math .toRadians(hAngle)));
12 floatz0=(float)(r*UNIT_SIZE*Math.sin(Math.toRadians(vAngle)));
13 float x1 = (float) (r * UNIT_SIZE //第2个顶点的坐标
14 * Math.cos(Math.toRadians(vAngle)) * Math.cos(Math
15 .toRadians(hAngle + angleSpan)));
16 float y1 = (float) (r * UNIT_SIZE
17 * Math.cos(Math.toRadians(vAngle)) * Math.sin(Math
18 .toRadians(hAngle + angleSpan)));
19 floatz1=(float)(r*UNIT_SIZE*Math.sin(Math.toRadians(vAngle)));
20 float x2 = (float) (r * UNIT_SIZE //第3个顶点的坐标
21 * Math.cos(Math.toRadians(vAngle + angleSpan)) * Math
22 .cos(Math.toRadians(hAngle + angleSpan)));
23 float y2 = (float) (r * UNIT_SIZE
24 * Math.cos(Math.toRadians(vAngle + angleSpan)) * Math
25 .sin(Math.toRadians(hAngle + angleSpan)));
26 float z2 = (float) (r * UNIT_SIZE * Math.sin(Math .toRadians(vAngle+ angleSpan)));
27 float x3 = (float) (r * UNIT_SIZE //第4个顶点的坐标
28 * Math.cos(Math.toRadians(vAngle + angleSpan)) * Math
29 .cos(Math.toRadians(hAngle)));
30 float y3 = (float) (r * UNIT_SIZE
31 * Math.cos(Math.toRadians(vAngle + angleSpan)) * Math
32 .sin(Math.toRadians(hAngle)));
33 float z3 = (float) (r * UNIT_SIZE * Math.sin(Math .toRadians(vAngle+ angleSpan)));
34 //将4个顶点的坐标按照卷绕成两个三角形的需要依次存入列表
35 alVertix.add(x1); alVertix.add(y1); alVertix.add(z1);
36 alVertix.add(x3); alVertix.add(y3); alVertix.add(z3);
37 alVertix.add(x0); alVertix.add(y0); alVertix.add(z0);
38 alVertix.add(x1); alVertix.add(y1); alVertix.add(z1);
39 alVertix.add(x2); alVertix.add(y2); alVertix.add(z2);
40 alVertix.add(x3); alVertix.add(y3); alVertix.add(z3);
41 }}
42 vCount = alVertix.size() / 3;//顶点的数量为坐标值数量的1/3,因为一个顶点有3个坐标
43 float vertices[] = new float[vCount * 3];
44 for (int i = 0; i < alVertix.size(); i++) {//将alVertix列表中的坐标值转存到一个float型数组中
45 vertices[i] = alVertix.get(i);
46 }
47 ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);//创建顶点坐标数据缓冲
48 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序
49 mVertexBuffer = vbb.asFloatBuffer(); //转换为int型缓冲
50 mVertexBuffer.put(vertices); //向缓冲区中放入顶点坐标数据
51 mVertexBuffer.position(0); //设置缓冲区起始位置
52 }
● 第3 行中的angleSpan 为将球进行经纬度方向单位切分的角度,角度越小,切分得就越细,绘制出来的形状也越接近于球。
● 第 4-41 行用双层 for 循环将球按照一定的角度跨度(angleSpan)沿纬度、经度方向进行切分。每次循环到一组纬度、经度时都将对应顶点看作小四边形的左上侧点,然后按照规律计算出小四边形中其他3个顶点的坐标,最后按照需要将用于卷绕成两个三角形的6个顶点的坐标依次存入列表。
● 第42-51 行首先将顶点坐标数据转存进数组中,然后再存入顶点数据缓冲中。
(3)完成了Java代码的开发后,就可以进行着色器的开发了。首先是顶点着色器,其具体代码如下。
代码位置:见随书光盘中源代码/第6章/Sample6_1/assets目录下的vertex.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵
2 attribute vec3 aPosition; //从管线接收的顶点位置
3 varying vec3 vPosition; //用于传递给片元着色器的顶点位置
4 void main() {
5 gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点的位置
6 vPosition = aPosition; //将原始顶点位置传递给片元着色器
7 }
提示
上述顶点着色器的代码与前面章节的案例基本一致,主要是增加了将顶点位置通过易变变量vPosition传递给片元着色器的相关代码。
(4)完成了顶点着色器的开发后,就可以开发本案例中实现棋盘着色的片元着色器了,其代码如下。
代码位置:见随书光盘中源代码/第6章/Sample6_1/assets目录下的frag.sh。
1 precision mediump float; //指定浮点相关变量的精度
2 uniform float uR; //从宿主程序中传入的球半径
3 varying vec3 vPosition; //接收从顶点着色器传递过来的顶点位置
4 void main() {
5 vec3 color;
6 float n = 8.0; //外接立方体每个坐标轴方向切分的份数
7 float span = 2.0*uR/n; //每一份的尺寸(小方块的边长)
8 int i = int((vPosition.x + uR)/span); //当前片元位置小方块的行数
9 int j = int((vPosition.y + uR)/span); //当前片元位置小方块的层数
10 int k = int((vPosition.z + uR)/span); //当前片元位置小方块的列数
11 int whichColor = int(mod(float(i+j+k),2.0));//计算当前片元行数、层数、列数的和并对2取模
12 if(whichColor == 1) { //奇数时为红色
13 color = vec3(0.678,0.231,0.129); //红色
14 }else { //偶数时为白色
15 color = vec3(1.0,1.0,1.0); //白色
16 }
17 gl_FragColor=vec4(color,0); //将计算出的片元颜色传递给管线
18 }
说明
上述片元着色器实现了如前一小节图6-4所示的棋盘着色器,其根据片元的位置计算出片元所在小方块的行数、层数、列数,再根据 3 个数之和的奇偶性确定片元所采用的颜色。
完成了上一节中球体的开发后,下面的部分将基于此球体逐步介绍光照各个方面的知识。具体计划为首先介绍OpenGL ES 2.0 中光照模型的基本知识,然后逐步为球体添加不同的光照效果。
如果要用一个数学模型完全真实地描述现实世界中的光照是很难的,一方面数学模型本身可能太过复杂;另一方面复杂的模型可能导致巨大的计算量。因此OpenGL ES 2.0 中采用的光照模型相对现实世界进行了很大的简化,将光照分成了3种组成元素(也可以称为3个通道),包括环境光、散射光以及镜面光,具体情况如图6-5所示。
提示
实际开发中,3个光照通道是分别采用不同的数学模型独立计算的,下面的几个小节将一一进行详细介绍。
环境光(Ambient)指的是从四面八方照射到物体上,全方位 360°都均匀的光。其代表的是现实世界中从光源射出,经过多次反射后,各方向基本均匀的光。环境光最大的特点是不依赖于光源的位置,而且没有方向性,图6-6简单地说明了这个问题。
从图6-6中可以看出,环境光不但入射是均匀的,反射也是各向均匀的。用于计算环境光的数学模型非常简单,具体公式如下。
环境光照射结果 = 材质的反射系数×环境光强度
了解了环境光的基本原理后,下面将通过一个简单的案例Sample6_2来介绍环境光效果的开发,案例的运行效果如图6-7所示。
说明
图6-7界面上部的滑块用来控制光源的位置,由于环境光的照射效果与光源位置无关,故无论光源调节到什么位置效果都相同。另外,实际开发中环境光强度一般都设置得较弱,因此仅用环境光照射的物体看起来并不是很清楚。
了解了案例的运行效果后,就可以进行代码的开发了。由于本案例主要是对上一节中Sample6_1的升级,因此这里仅给出变化较大且有代表性的部分,具体如下所列。
(1)首先需要修改的是MySurfaceView类中的onDrawFrame方法,具体代码如下。
代码位置:见随书光盘中源代码/第6章/Sample6_2/com/bn/Sample6_2目录下的MySurfaceView.java。
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代码的修改后,接着就可以开发着色器了。首先是顶点着色器,其代码如下。
代码位置:见随书光盘中源代码/第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)开发完顶点着色器后,接下来就可以开发片元着色器了,其具体代码如下。
代码位置:见随书光盘中源代码/第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 }
说明
上述片元着色器代码与前面案例的基本相同,主要是增加了接收环境光强度以及使用环境光强度与片元本身颜色值加权计算产生最终片元颜色值的相关代码。
上一小节中给出了仅仅使用环境光进行照射的案例,读者可能觉得效果并不好。确实如此,仅仅有环境光的场景效果是很差的,没有层次感。本节将介绍另外一种真实感好很多的光照效果——散射光(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进行升级,增加存储当前光源位置的相关成员变量以及设置光源位置的方法,具体代码如下。
代码位置:见随书光盘中源代码/第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类,主要是增加初始化法向量数据以及将法向量数据传入渲染管线的相关代码,具体情况如下。
代码位置:见随书光盘中源代码/第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方法,具体代码如下。
代码位置:见随书光盘中源代码/第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代码的修改就基本完成了,下面需要修改的是着色器的代码。首先是顶点着色器,其代码如下。
代码位置:见随书光盘中源代码/第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)完成了顶点着色器的开发后,就可以开发片元着色器了,其代码如下。
代码位置:见随书光盘中源代码/第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行在将片元颜色值传递给渲染管线之前,增加了其与散射光最终强度加权计算的操作。
使用了上一小节中介绍的散射光效果后,场景的整体效果有了较大的提升。但这并不是光照的全部,现实世界中当光滑表面被照射时会有方向很集中的反射光。这就是镜面光(Specular),本小节将详细介绍镜面光的计算模型。
与散射光最终强度仅依赖于入射光与被照射点法向量的夹角不同,镜面光的最终强度还依赖于观察者的位置。也就是说,如果从摄像机到被照射点的向量不在反射光方向集中的范围内,观察者将不会看到镜面光,图6-12简单地说明了这个问题。
镜面光的计算模型比前面的两种光都要复杂一些,具体公式如下。
实际开发中往往分两步进行计算,此时公式被拆解为如下情况。
镜面光照射结果=材质的反射系数×镜面光最终强度
提示
材质的反射系数实际指的就是物体被照射处的颜色,镜面光强度指的是镜面光中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类中设置摄像机的方法进行升级并增加相关的成员变量,具体代码如下。
代码位置:见随书光盘中源代码/第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类,主要是增加了将摄像机位置传送进渲染管线的相关代码,具体内容如下。
代码位置:见随书光盘中源代码/第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代码的修改就基本完成了,下面需要修改的是着色器的代码。首先是顶点着色器,其代码如下。
代码位置:见随书光盘中源代码/第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)完成了顶点着色器的开发后,就可以开发片元着色器了,其代码如下。
代码位置:见随书光盘中源代码/第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 }
说明
上述片元着色器的代码与散射光的套路基本一致,只是将接收散射光最终强度并与片元颜色值加权计算换成了接收镜面光最终强度并与片元颜色值加权计算。
前面3个小节案例中的每个仅采用了一种光照通道,而现实世界中3种通道是同时作用的。因此本小节将通过一个案例Sample6_5将前面3个小节不同通道(环境光、散射光、镜面光)的光照效果综合起来,其运行效果如图6-15所示。
提示
从图6-15中可以看出,综合了3种光照通道后,场景的真实感大大增强。
了解了案例的运行效果后,就可以进行案例的开发了。由于实际上本案例仅仅是将案例Sample6_2、Sample6_3和Sample6_4中顶点着色器及片元着色器计算光照的相关代码进行了综合,因此这里仅给出综合后着色器的代码。
(1)首先综合了3种光照通道的顶点着色器,其具体代码如下。
代码位置:见随书光盘中源代码/第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)完成了顶点着色器的开发后,就可以开发片元着色器了,其代码如下。
代码位置:见随书光盘中源代码/第6章/Sample6_5/assets目录下的frag.sh。
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个通道影响的最终片元颜色值后再将其传递给渲染管线。
前面的案例中都是将光各个通道的原始强度固化在着色器程序中的,如果有需要,读者可以自行开发出将光各个通道的强度由宿主程序传入着色器一致变量的版本。另外前面的案例中也仅有一个光源,如果有需要,读者可以根据前面案例介绍的内容开发出多个光源叠加照射的效果。
还需要注意的是,本节介绍的光照计算模型是比较常用的也是比较简单的一套,还有很多其他更为复杂的可以取得更好效果的光照计算模型,读者有需要也可以进一步参考其他技术资料。
上一节中介绍的光照效果都是基于定位光光源的,定位光光源类似于现实生活中的白炽灯灯泡,其在某个固定的位置,发出的光向四周发散。定位光照射的一个明显特点就是,在给定光源位置的情况下,对不同位置的物体产生的光照效果不同。
现实世界中并不都是定位光,例如照射到地面上的阳光,光线之间是平行的,这种光称为定向光。定向光照射的明显特点是,在给定光线方向的情况下,场景中不同位置的物体反映出的光照效果完全一致。图6-16中对定位光与定向光的照射特点进行了比较。
了解了定位光与定向光的区别后,接下来将通过一个案例Sample6_6介绍定向光效果的开发,其运行效果如图6-17所示。
说明
图6-17中左侧的图表示定向光方向从左向右照射的情况,右侧的图表示定向光方向从右向左照射的情况。从左右两幅效果图的对比中可以看出,在定向光方向确定的情况下,其对场景中任何位置的物体都产生相同的光照效果。
了解了定向光的基本原理及案例的运行效果后,就可以进行案例的开发了。由于实际上本案例仅仅是将案例Sample6_5复制了一份并进行了修改,因此这里仅给出修改的主要步骤,具体如下所列。
(1)由于使用定向光需要将光线的方向由 Java 程序传入渲染管线,为了方便首先需要对MatrixState类进一步升级,在其中增加设置定向光方向并存入缓冲的相关代码,具体情况如下。
代码位置:见随书光盘中源代码/第6章/Sample6_6/com/bn/Sample6_6目录下的MatrixState.java。
1 public static float[] lightDirection = new float[] { 0, 0, 1 };//定向光方向向量数组
2 public static FloatBuffer lightDirectionFB; //定向光方向向量数据缓冲
3 public static void setLightDirection(float x,float y,float z){//设置定向光方向的方法
4 llbbL.clear(); //清空缓冲
5 //将定向光方向向量的X、Y、Z分量存入数组
6 lightDirection[0] = x;lightDirection[1] = y;lightDirection[2] = z;
7 llbbL.order(ByteOrder.nativeOrder()); //设置字节顺序
8 lightDirectionFB = llbbL.asFloatBuffer(); //转换为float型缓冲
9 lightDirectionFB.put(lightDirection); //将光源方向向量放入数据缓冲
10 lightDirectionFB.position(0); //设置缓冲区起始位置
11 }
● 第1-2 行为该类中新增加的成员变量,包括定向光方向向量数组lightDirection、定向光方向向量数据缓冲lightDirectionFB。
● 第3-11 行为设置定向光方向的方法,该方法首先将接收的定向光方向向量x、y、z 轴的分量依次存入数组,然后再将数组中的值存入缓冲,供绘制时传入渲染管线。
(2)接着需要修改的是Ball类,在该类中增加了将定向光方向传入渲染管线的相关代码,具体情况如下。
代码位置:见随书光盘中源代码/第6章/Sample6_6/com/bn/Sample6_6目录下的Ball.java。
1 public class Ball {
2 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
3 int muLightDirectionHandle; //定向光方向一致变量的引用
4 public Ball(MySurfaceView mv) {/*代码省略*/}
5 public void initVertexData() {/*代码省略*/}
6 public void initShader(MySurfaceView mv) { //初始化着色器
7 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
8 muLightDirectionHandle=GLES20.glGetUniformLocation(
9 mProgram, "uLightDirection"); //获取着色器中定向光方向一致变量的引用
10 }
11 public void drawSelf() {
12 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
13 GLES20.glUniform3fv( //将定向光方向传入渲染管线
14 muLightDirectionHandle, 1, MatrixState.lightDirectionFB);
15 ……//此处省略了部分代码,请读者自行查看随书光盘中的源代码
16 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount); //绘制球体
17 }}
● 第3 行增加了声明定向光方向一致变量引用muLightDirectionHandle的代码。
● 第8-9 行在初始化着色器的方法中增加了获取定向光方向一致变量引用值的代码。
● 第11-17行为绘制球的drawSelf 方法,其中增加了将定向光方向传送进渲染管线的代码。
(3)完成了Ball类的修改后,对Java代码的修改就基本完成了,下面就可以修改着色器的代码了。由于本案例中的片元着色器与Sample6_5中的相同,因此这里仅介绍顶点着色器,其具体代码如下。
代码位置:见随书光盘中源代码/第6章/Sample6_6/assets目录下的vertex.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵
2 uniform mat4 uMMatrix; //变换矩阵
3 uniform vec3 uLightDirection; //定向光方向
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 directionalLight( //定向光光照计算的方法
12 in vec3 normal, //法向量
13 inout vec4 ambient, //环境光最终强度
14 inout vec4 diffuse, //散射光最终强度
15 inout vec4 specular, //镜面光最终强度
16 in vec3 lightDirection, //定向光方向
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 vec3 vp= normalize(lightDirection); //规格化定向光方向向量
28 vec3 halfVector=normalize(vp+eye); //求视线与光线的半向量
29 float shininess=50.0; //粗糙度,越小越光滑
30 float nDotViewPosition=max(0.0,dot(newNormal,vp)); //求法向量与vp的点积与0的最大值
31 diffuse=lightDiffuse*nDotViewPosition; //计算散射光的最终强度
32 float nDotViewHalfVector=dot(newNormal,halfVector);//法线与半向量的点积
33 float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess));//镜面反射光强度因子
34 specular=lightSpecular*powerFactor; //计算镜面光的最终强度
35 }
36 void main(){
37 gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点位置
38 vec4 ambientTemp,diffuseTemp,specularTemp; //用来接收3个通道最终强度的变量
39 directionalLight(normalize(aNormal),ambientTemp,diffuseTemp,specularTemp,uLightDirection,
40 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));//计算定向光各通道强度
41 vAmbient=ambientTemp; //将环境光最终强度传给片元着色器
42 vDiffuse=diffuseTemp; //将散射光最终强度传给片元着色器
43 vSpecular=specularTemp; //将镜面光最终强度传给片元着色器
44 vPosition = aPosition; //将顶点的位置传给片元着色器
45 }
● 第3 行将原来定位光光源位置一致变量的声明替换成了定向光方向向量一致变量的声明。
● 第 11-35 行将原来计算定位光光照的 pointLight 方法替换成了计算定向光光照的directionalLight 方法。上述两个方法主要有两点不同,首先是原来表示定位光光源位置的参数lightLocation被换成了表示定向光方向的参数lightDirection。另一个是计算时所需的光方向向量直接从lightDirection参数规格化即可得到,不需要再通过光源位置与被照射点位置进行计算了。
提示
从上述顶点着色器的代码中可以看出,与定位光相同,定向光也是分环境光、散射光、镜面光3个通道进行计算的。
本章前面几节的案例都是基于球面开发的,球面属于连续、平滑的曲面,因此面上的每个顶点都有确定的法向量。但现实世界中的物体表面并不都是连续、平滑的,此时对于面上的某些点的法向量计算就不那么直观了,图6-18说明了这个问题。
从图6-18中可以看出,顶点A位于长方体左、上、前3个面的交界处,此处是不光滑的。这种情况下顶点A的法向量有两种处理策略,具体如下所列。
● 在顶点 A 的位置放置 3 个不同的顶点,每个顶点看作是仅属于一个面。各个顶点的法向量即为其属于的面的法向量,这种策略就是面法向量的策略,比较适合棱角分明的物体。
● 顶点 A 的位置仅认为存在一个顶点,其法向量取其所属的所有面法向量的平均值。这种策略就是点法向量策略,比较适合用多个平面搭建平滑曲面的情况。
提示
前面球体的案例采用的就是此策略,只不过由于球面上顶点的法向量可以直接计算,因此相当于直接得到了点平均法向量,而略去了计算平均值的过程。但很多情况下是需要进行平均法向量的计算的,尤其是在加载预制3D模型的时候,这一点在后面的章节会有专门的介绍。
了解了点法向量和面法向量的基本知识后,下面将通过两个基本相同的绘制立方体的案例(Sample6_7 和 Sample6_8)对这两种策略进行比较。这两个案例的运行效果如图 6-19 和图 6-20所示,其中图6-19来自采用面法向量策略的案例Sample6_7,图6-20来自采用点法向量策略的案例Sample6_8。
说明
从图6-19与图6-20的比较中可以看出,对于棱角分明的物体适合采用面法向量策略。若采用点法向量策略渲染真实感就会差很多了,实际开发中读者应该根据所绘制物体表面的特点来选用合适的策略。
了解了两个案例的运行效果后,就可以进行案例的开发了。由于这两个案例中大部分的代码与前面6.2.5小节中案例Sample6_5里的完全相同,主要的区别就在立方体顶点与法向量初始化的部分,所以下面仅给出Sample6_7与Sample6_8中初始化立方体顶点与法向量的部分代码,具体内容如下所列。
(1)首先介绍采用面法向量策略的案例 Sample6_7 中初始化立方体顶点及法向量数据的initVertexData方法,其代码如下。
代码位置:见随书光盘中源代码/第6章/Sample6_7/com/bn/Sample6_7目录下的Cube.java。
1 public void initVertexData() { //初始化顶点数据的方法
2 vCount=6*6; //顶点数(6个面,每个面两个三角形,6个顶点)
3 float vertices[]=new float[]{
4 Constant.UNIT_SIZE,Constant.UNIT_SIZE,Constant.UNIT_SIZE, //立方体前面
5 -Constant.UNIT_SIZE,Constant.UNIT_SIZE,Constant.UNIT_SIZE, //两个三角形
6 -Constant.UNIT_SIZE,-Constant.UNIT_SIZE,Constant.UNIT_SIZE, //中6个顶点
7 Constant.UNIT_SIZE,Constant.UNIT_SIZE,Constant.UNIT_SIZE,//的x、y、z坐标
8 -Constant.UNIT_SIZE,-Constant.UNIT_SIZE,Constant.UNIT_SIZE,
9 Constant.UNIT_SIZE,-Constant.UNIT_SIZE,Constant.UNIT_SIZE,
10 ……//此处省略了其他5个面顶点坐标产生的代码,
11 ……//需要的读者请自行查看随书光盘中的源代码
12 };
13 ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);//创建顶点坐标数据缓冲
14 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序
15 mVertexBuffer = vbb.asFloatBuffer(); //转换为float型缓冲
16 mVertexBuffer.put(vertices); //向缓冲区中放入顶点坐标数据
17 mVertexBuffer.position(0); //设置缓冲区起始位置
18 float normals[]=new float[]{
19 0,0,1, 0,0,1, 0,0,1, 0,0,1, 0,0,1, 0,0,1, //前面上6个顶点的法向量
20 0,0,-1, 0,0,-1, 0,0,-1, 0,0,-1, 0,0,-1, 0,0,-1,//后面上6个顶点的法向量
21 -1,0,0, -1,0,0, -1,0,0, -1,0,0, -1,0,0, -1,0,0,//左面上6个顶点的法向量
22 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, //右面上6个顶点的法向量
23 0,1,0, 0,1,0, 0,1,0, 0,1,0, 0,1,0, 0,1,0, //上面上6个顶点的法向量
24 0,-1,0, 0,-1,0, 0,-1,0, 0,-1,0, 0,-1,0, 0,-1,0,//下面上6个顶点的法向量
25 };
26 ByteBuffer nbb = ByteBuffer.allocateDirect(normals.length*4);//创建绘制顶点法向量缓冲
27 nbb.order(ByteOrder.nativeOrder()); //设置字节顺序
28 mNormalBuffer = nbb.asFloatBuffer(); //转换为float型缓冲
29 mNormalBuffer.put(normals); //向缓冲区中放入顶点法向量数据
30 mNormalBuffer.position(0); //设置缓冲区起始位置
31 }
● 第3-17 行为立方体各顶点位置数据的初始化。
● 第17-30 行为立方体各顶点法向量数据的初始化,其中给每个顶点设置了其所属面的法向量。
(2)接着介绍采用点法向量策略的案例 Sample6_8 中初始化立方体顶点及法向量数据的initVertexData方法,其代码如下。
代码位置:见随书光盘中源代码/第6章/Sample6_8/com/bn/Sample6_8目录下的Cube.java。
1 public void initVertexData() { //初始化顶点数据的方法
2 vCount = 6 * 6; //顶点数
3 float vertices[] = new float[] {
4 ……//此处省略了初始化立方体6个面各个顶点坐标数据的代码,
5 ……//需要的读者请自行查看随书光盘中的源代码
6 };
7 ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);//创建顶点坐标数据缓冲
8 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序
9 mVertexBuffer = vbb.asFloatBuffer(); //转换为float型缓冲
10 mVertexBuffer.put(vertices); //向缓冲区中放入顶点坐标数据
11 mVertexBuffer.position(0); //设置缓冲区起始位置
12 float normals[] = vertices; //顶点法向量数据
13 ByteBuffer nbb = ByteBuffer.allocateDirect(normals.length * 4);//创建绘制顶点法向量缓冲
14 nbb.order(ByteOrder.nativeOrder()); //设置字节顺序
15 mNormalBuffer = nbb.asFloatBuffer(); //转换为float型缓冲
16 mNormalBuffer.put(normals); //向缓冲区中放入顶点法向量数据
17 mNormalBuffer.position(0); //设置缓冲区起始位置
18 }
提示
由于本案例中原始情况下将立方体的几何中心放在了坐标原点,因此,每个顶点的平均法向量就没有必要真正进行求和再平均的计算了,直接采用顶点的x、y、z坐标代替即可。但开发中并不总是像本章的案例一样可以进行简化,后面的章节会给出需要详细计算的案例。
细心的读者会发现,本章前面的案例都是在顶点着色器中进行光照计算的。这是由于在顶点着色器中对每个顶点进行光照计算后得到顶点的最终光照强度,再由管线插值后传入片元着色器以计算片元的颜色,这样一方面效率比较高;另一方面产生的光照效果也不错。
但由于这种计算方式插值的是基于顶点计算后的光照强度,因此在要求很高,希望有非常细腻光照效果的场合下就略显粗糙了。本节将介绍另一种光照计算方式,其首先将插值后的法向量数据传入片元着色器,然后在片元着色器中进行光照计算。这种新的方式也称为每片元光照,可以取得为更细腻的光照效果。
进行案例开发之前需要首先了解一下本节两个案例(Sample6_9和Sample6_10)的运行效果,具体情况如图6-21所示。
说明
图6-21中左侧是每片元计算一次光照的案例Sample6_9的运行效果,右侧是每顶点计算一次光照的案例Sample6_10的运行效果。从两幅图的对比中可以看出,每片元执行一次光照使过渡更平滑,没有明显的边缘。另外,仅从图上观察可能区别还不是很明显,笔者建议读者用真机运行一下两个案例,将光源设置在不同的位置观察比较,区别会更明显。
了解了两个案例的运行效果后,就可以进行开发了。实际上这两个案例主要是将前面 6.2.5小节中的案例Sample6_5复制并修改了部分代码而成的。其中Sample6_10仅修改了Java代码中切割球面的角度以及绘制球体的次数,没有本质变化,这里不再赘述,需要的读者请参考随书光盘中的源代码。
而案例Sample6_9除了也进行了Sample6_10的Java代码改动外,还大面积修改了顶点着色器与片元着色器,具体情况如下所列。
(1)首先介绍Sample6_9中修改后的顶点着色器,其具体代码如下。
代码位置:见随书光盘中源代码/第6章/Sample6_9/assets目录下的vertex.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵
2 attribute vec3 aPosition; //顶点位置
3 attribute vec3 aNormal; //法向量
4 varying vec3 vPosition; //用于传递给片元着色器的顶点位置
5 varying vec3 vNormal; //用于传递给片元着色器的法向量
6 void main(){
7 gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点位置
8 vPosition = aPosition; //将顶点的位置传给片元着色器
9 vNormal = aNormal; //将法向量传给片元着色器
10 }
提示
从上述代码中可以看出,顶点着色器比改动前简单多了,没有了计算光照的大量代码,同时增加了将法向量通过易变变量传入片元着色器的代码。
(2)介绍完顶点着色器后,接着就应该介绍改动后的片元着色器了,其具体代码如下。
1 precision mediump float; //给出默认浮点精度
2 uniform float uR; //球的半径
3 uniform vec3 uLightLocation; //光源位置
4 uniform mat4 uMMatrix; //变换矩阵
5 uniform vec3 uCamera; //摄像机位置
6 varying vec3 vPosition; //接收从顶点着色器传递过来的顶点位置
7 varying vec3 vNormal; //接收从顶点着色器传递过来的法向量
8 void pointLight( //定位光光照计算的方法
9 in vec3 normal, //法向量
10 inout vec4 ambient, //环境光最终强度
11 inout vec4 diffuse, //散射光最终强度
12 inout vec4 specular, //镜面光最终强度
13 in vec3 lightLocation, //光源位置
14 in vec4 lightAmbient, //环境光强度
15 in vec4 lightDiffuse, //散射光强度
16 in vec4 lightSpecular //镜面光强度
17 ){
18 ambient=lightAmbient; //直接得出环境光的最终强度
19 vec3 normalTarget=vPosition+normal; //计算变换后的法向量
20 vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(vPosition,1)).xyz;
21 newNormal=normalize(newNormal); //对法向量规格化
22 //计算从表面点到摄像机的向量
23 vec3 eye= normalize(uCamera-(uMMatrix*vec4(vPosition,1)).xyz);
24 //计算从表面点到光源位置的向量vp
25 vec3 vp= normalize(lightLocation-(uMMatrix*vec4(vPosition,1)).xyz);
26 vp=normalize(vp);//格式化vp
27 vec3 halfVector=normalize(vp+eye); //求视线与光线的半向量
28 float shininess=50.0; //粗糙度,越小越光滑
29 floatnDotViewPosition=max(0.0,dot(newNormal,vp));//求法向量与vp的点积与0的最大值
30 diffuse=lightDiffuse*nDotViewPosition; //计算散射光的最终强度
31 float nDotViewHalfVector=dot(newNormal,halfVector);//法线与半向量的点积
32 float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess));//镜面反射光强度因子
33 specular=lightSpecular*powerFactor; //计算镜面光的最终强度
34 }
35 void main() {
36 ……//此处省略了计算片元颜色值的代码,请读者自行查看随书光盘中的源代码
37 vec4 ambient,diffuse,specular; //用来接收3个通道最终强度的变量
38 pointLight(normalize(vNormal),ambient,diffuse,specular,uLightLocation,//计算定位光各通道强度
39 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));
40 //综合3个通道光的最终强度及片元的颜色计算出最终片元的颜色并传递给管线
41 gl_FragColor=finalColor*ambient + finalColor*diffuse + finalColor*specular;
42 }
提示
读者应该发现,上述片元着色器中的很多代码都是本章前面案例中多次出现过的,只不过前面的案例中都是在顶点着色器中出现,而这里挪到了片元着色器中。因此,每片元计算光照与每顶点计算光照算法并没有本质区别,只是代码执行的位置不同、效果与效率不同而已。实际开发中读者应该权衡速度、效果的要求,选用合适的计算策略。
本章主要向读者介绍了OpenGL ES 2.0 中光照的基本知识,掌握了本章所介绍的技术后,应该能开发出更加逼真的3D场景。
提示
读者可能会发现,本章中光照的案例都是没有阴影的,而阴影对于增加场景的真实感是非常重要的。基本光照是不会自动产生阴影的,实际上阴影是3D开发中一个比较高级的话题,后面的章节会进行专门讨论。