本文为《WebGL编程指南》第八章读书笔记
总目录链接:https://blog.csdn.net/floating_heart/article/details/124001572
本文包括以下内容:
- 介绍了点光源、平行光的漫反射模型和环境光反射模型
- 在着色器中根据光照模型构建公式,实现平行光和点光源的光照效果,JavaScript为着色器提供数据支持
- 分别在顶点着色器和片元着色器中计算颜色值,展示逐顶点计算和逐片元计算的不同:逐片元的光照效果更加自然。
这一章主要讨论光照问题,讨论如何在三维场景中实现不同类型的光照以及其产生的效果。光照使场景更具有层次感,如果你希望建立逼真的三维场景,就应当使用光照。
这一章的主要内容如下:
在这一章结束时,你将具有足够多的知识去实现三维场景中的光照效果。
这一章节理论部分几乎每一句话都很重要,此处仅保留其梗概,以供后期参考。
相关内容:1. 光照的基本原理,提出着色和阴影,反射光的公式;2. 静止图像的光照效果,计算平行光(漫反射)和环境光(环境反射,在物理上也是漫反射)
GLSL ES函数:normalize()归一化;dot()矢量点积;max()取大值细节:1.已知的访问vec4类型数据各个分量的索引有xyzw和rgba,可以vec4.x访问单一值,也可以vec4.xyz访问组合值。2. 我们通过GLSL ES中的数学函数计算处颜色,再进行着色,所以是通过旧的功能获得新的效果。
学过物理的都应该知道,我们能看到物体是因为光线照到物体上后反射的光进入了眼睛,一切的视觉渲染从此开始。
在现实世界,光纤照射到物体上时,发生了两个重要现象:
正是物体的阴影和表面的明暗差异给了我们立体的视觉效果。
本章主要讨论前者,在讨论着色过程之前,需要考虑以下两点:
光源类型
真实世界中的光主要有两种类型:
此外:
三维图形学还使用一些其他类型的光,如聚光灯光(spot light)来模拟电筒、车前灯等。本书只讨论前三种基本类型的光,其它光源类型可以参考OpenGL ES 2.0 Programming Guide一书。
本书讨论如图三种类型的光源。
平行光: 顾名思义,平行光的光线是相互平行的,平行光具有方向。平行光可以看作是无限远处的光源(比如太阳)发出的光。因为太阳距离地球很远,所以阳光到达地球时可以认为是平行的。平行光很简单,可以用一个方向和一个颜色(此处的颜色已经包含强度,如(1,1,1)和(2,2,2)强度差距两倍)来定义。
点光源光:点光源光是从一个点向周围的所有方向发出的光。点光源光可以用来表示现实中的灯泡、火焰等。我们需要指定点光源的位置和颜色。光线的方向将根据点光源的位置和被照射之处的位置计算出来,因为点光源的光线的方向在场景内的不同位置是不同的。(本书未进行光源强度的衰减,可参考OpenGL ES 2.0 Programming Guide一书)
环境光: 环境光(间接光)是指那些经光源(点光源或平行光源)发出后,被墙壁等物体多次反射,然后照到物体表面上的光。环境光从各个角度照射物体,其强度都是一致的。比如说,在夜间打开冰箱的门,整个厨房都会有些微微亮,这就是环境光的作用。 环境光不用指定位置和方向,只需要指定颜色即可。(暂时如此认为,不计算多次反射的光)
反射类型
物体反射光的方向和颜色与入射光和物体表面类型有关。入射光信息包括入射光方向和颜色,物体表面信息包括表面的固有颜色(基底色)和反射特性。物体表面反射光线有两种:漫反射(diffuse reflection)和环境反射(envirment ambient reflection)。
本节需要考虑如何根据入射光和物体表面类型计算反射光颜色。(此处没有讨论镜面反射。)
漫反射是针对平行光或点光源而言的。漫反射的反射光在各个方向上是均匀的。如果物体表面像镜子一样光滑,那么光线就会以特定的角度反射出去(镜面反射);但现实中的大部分材质,比如纸张、岩石、塑料等,其表面都是粗糙的,在这种情况下反射光就会以不固定的角度反射出去。漫反射就是针对后一种情况而建立的理想反射模型。
在漫反射中,反射光的颜色取决于入射光的颜色、表面的基底色、入射光与表面形成的入射角。我们将人射角定义为入射光与表面的法线形成的夹角,并用θ表示,漫反射光的颜色可以根据下式计算得到:
< 漫 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > × cos θ <漫反射光颜色>=<入射光颜色>\times<表面基底色>\times\cos\theta <漫反射光颜色>=<入射光颜色>×<表面基底色>×cosθ
式中:
环境反射是针对环境光而言的。在环境反射中,反射光的方向可以认为就是入射光的反方向(注意是可以认为)。由于环境光照射物体的方式就是各方向均匀、强度相等的,所以反射光也是各向均匀的。我们可以这样来描述它:
< 环 境 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > < 入 射 光 颜 色 > 就 是 环 境 光 颜 色 <环境反射光颜色>=<入射光颜色>\times<表面基底色>\\ <入射光颜色>就是环境光颜色 <环境反射光颜色>=<入射光颜色>×<表面基底色><入射光颜色>就是环境光颜色
< 表 面 的 反 射 光 颜 色 > = < 漫 反 射 光 颜 色 > + < 环 境 反 射 光 颜 色 > <表面的反射光颜色>=<漫反射光颜色>+<环境反射光颜色> <表面的反射光颜色>=<漫反射光颜色>+<环境反射光颜色>
我们可以使用简单的加法进行二者叠加,但需要注意,两种反射光并不一定总是存在,也不一定要完全按照上述公式来进行计算,我们可以根据项目需要来修改公式,达到想要的效果。
本节准备建立一个示例程序,在合适的位置放置一个光源,对场景进行着色。首先对示例的光照情况进行分析。
平行光下的漫反射
< 漫 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > × cos θ <漫反射光颜色>=<入射光颜色>\times<表面基底色>\times\cos\theta <漫反射光颜色>=<入射光颜色>×<表面基底色>×cosθ
分析漫反射光,需要前述公式中的三项数据:入射光的颜色、表面基底色和入射角θ。
这几项数据中,入射光的颜色(包括强度)采用RGB值来表示,比如标准强度的白光的颜色值是(1.0,1.0,1.0);表面的基底色是物体本身的颜色,或者说物体在标准白光下的颜色。
假设入射光是白色(1.0,1.0,1.0),物体表面基底色是红色(1.0,0.0,0.0),入射角度θ为0.0(垂直入射),此时漫反射光的颜色为红色:
( 1.0 , 1.0 , 1.0 ) × ( 1.0 , 0.0 , 0.0 ) × cos 0 ° = ( 1.0 , 0.0 , 0.0 ) (1.0,1.0,1.0)\times(1.0,0.0,0.0)\times\cos0°=(1.0,0.0,0.0) (1.0,1.0,1.0)×(1.0,0.0,0.0)×cos0°=(1.0,0.0,0.0)
根据光线和表面的方向计算入射角
在实际操作中,我们只知道光线的方向和物体表面的朝向,需要根据二者计算出光线入射角。
根据矢量点积(内积)的性质,我们可以简单获得入射角的余弦值。
cos θ = < 光 线 方 向 > ⋅ < 法 线 方 向 > \cos\theta=<光线方向>·<法线方向> cosθ=<光线方向>⋅<法线方向>
对矢量n和l进行点积运算的公式为: n ⋅ l = ∣ n ∣ × ∣ l ∣ × cos θ n·l=|n|\times|l|\times\cos\theta n⋅l=∣n∣×∣l∣×cosθ
假设矢量n为(nx,ny,nx),l为(lx,ly,lz),那么: n ⋅ l = n x ∗ l x + n y ∗ l y + n z ∗ l z n·l=nx*lx+ny*ly+nz*lz n⋅l=nx∗lx+ny∗ly+nz∗lz
此时需要遵循如下规定:
获取法线:表面的朝向
这一部分中文书中有些错误,建议参考英文书
物体表面的朝向,就是垂直于表面的方向,也称作法线或法向量。
关于法向量,相信各位读者都有了解,笔记结合书中内容进行一定总结:
法向量可以通过平面内不共线的两个矢量的叉乘获得,此处书中没有提到。
下图展示了立方体中各个平面的法向量,每个顶点分属不同的平面,所以一个顶点有三个法向量:
示例程序LightedCube.js
通过以上说明,漫反射光颜色即可计算得出。此处示例程序绘制了一个处于白色平行光照射下的红色三角形。
示例代码基于ColoredCube.js
改写,原程序每一个顶点定义了三次,具有不同的颜色,顶点和颜色通过两个缓冲区保存,顶点通过索引进行绘制。
下面对一些重要的地方进行说明:
为了计算漫反射光,引入了光线颜色、光线方向和法向量,将归一化和点积计算放到了着色器中,具体如下:
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightDirection;\n' + // 光线方向(归一化的世界坐标)
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行归一化
' vec3 normal = normalize(vec3(a_Normal));\n' +
// 计算光线方向和法向量的点积
' float nDotL = max(dot(u_LightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;\n' +
' v_Color = vec4(diffuse,a_Color.a);\n' +
'}\n'
补充一:max(dot(u_LightDirection,normal),0.0)将小于0的值自动归为0,即入射角θ大于90度,光线照射不到(照到了背面)的情况,反射光视为0。
所以,本例是一个简单的例子,没有考虑物体遮挡光源的情况。
补充二:示例中法向量从vec4到vec3数据类型发生了变换,之所以要接收vec4类型的数据是为了方便后面扩展。
补充三:此处通过a_Color.a访问vec4类型数据的第四个分量,已知的访问vec4类型数据各个分量的索引有xyzw和rgba。
补充四:本例暂时认为物体都是不透明的,方便计算。
示例主要的改动都在顶点着色器中体现了,在JavaScript中只需要提供着色器相关支持即可。
// main()
// 光线颜色
let u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor')
if (!u_LightColor) {
console.log('Failed to get the storage loaction of u_LightColor')
return
}
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0)
注意需要归一化!
// main()
// 光线方向(世界坐标系)
let u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection')
if (!u_LightDirection) {
console.log('Failed to get the storage loaction of u_LightDirection')
return
}
let lightDirection = new Vector3([0.5, 3.0, 4.0])
lightDirection.normalize() // 归一化
gl.uniform3fv(u_LightDirection, lightDirection.elements)
// initVertexBuffers()
...
// 法向量
var normals = new Float32Array([
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0,
0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0,
0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0
]);
...
// 将顶点法向量写入缓冲区并开启分配
if (!initArrayBuffer(gl, normals, 3, gl.FLOAT, 'a_Normal')) {
return -1
}
...
为立方体补充环境光——示例LightedCube_ambient,js
示例LightedCube_ambient,js
在上一个示例的基础上,加入了环境光的影响,使其更符合现实环境。
可以看到,示例图像整体变亮了很多。
新的示例在原示例基础上加入了环境光变量,主要改动如下:
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightDirection;\n' + // 光线方向(归一化的世界坐标)
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行归一化
' vec3 normal = normalize(vec3(a_Normal));\n' +
// 计算光线方向和法向量的点积
' float nDotL = max(dot(u_LightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
// 计算环境光产生的反射光颜色
' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
// 最终颜色
' v_Color = vec4(diffuse + ambient,a_Color.a);\n' +
'}\n'
// 环境光
let u_AmbientLight = gl.getUniformLocation(gl.program,'u_AmbientLight')
if (!u_AmbientLight) {
console.log('Failed to get the storage loaction of u_AmbientLight')
return
}
gl.uniform3f(u_AmbientLight, 0.2,0.2,0.2)
相关内容:实现运动物体的光照效果:求新的法线;
相关函数:1. Matrix4对象的两种方法:setInverseOf()获得参数(也是Matrix4对象)的逆矩阵,transpose()对自身转置。小结:可以使用模型矩阵子矩阵或模型矩阵的逆转置矩阵计算模型变换后的法线方向
在上一个示例中,模型矩阵为单位矩阵,立方体的坐标没有发生几何变换。假如物体发生运动,影响反射光的因素中法线方向也会随之变动,影响物体的光照效果。本例考虑如何在物体运动的时候,获取法线方向,视线光照效果。
模型矩阵的逆转置矩阵计算法线方向
首先给出结论,模型矩阵的逆转置矩阵可以用于计算新的法线方向,说明如下。
我们接触的物体坐标变换主要有三种方式:平移、旋转和缩放。
对于法线方向发生变化的情况,为了获得新的法线方向,我们有时可以求诸于模型矩阵自身,但总是可以依靠模型矩阵的逆转置矩阵。
从模型矩阵自身求解:
“从模型矩阵自身求解”是指使用模型矩阵左上角3×3的子矩阵对原法线方向进行变换,获得新的法线方向。子矩阵中包含了物体旋转和缩放的信息,适用的情况如下:
模型矩阵的逆转置矩阵:
首先,可以考虑为什么使用逆转置矩阵?(内容来自书中附录E,做了一定简化,已了解的学者可以跳过)
此处,令模型矩阵为M,初始法向量为n,垂直于原始法向量n的向量为s,令变换法向量的变换矩阵为M‘,如上图所示。此时可以获得如下关系:
n ′ = M ′ × n s ′ = M × s n'=M'\times n\\ s'=M\times s n′=M′×ns′=M×s
已知两个相互垂直的矢量点积为0,所以:
n ′ ⋅ s ′ = 0 n'·s'=0 n′⋅s′=0
将第一组等式带入第二组,可得:
( M ′ × n ) ⋅ ( M × s ) = 0 ( M ′ × n ) T × ( M × s ) = 0 n T × M ′ T × M × s = 0 (M'\times n)·(M\times s)=0\\ (M'\times n)^T\times(M\times s)=0\\ n^T\times M'^T\times M\times s=0 (M′×n)⋅(M×s)=0(M′×n)T×(M×s)=0nT×M′T×M×s=0
因为n和s相互垂直,所以:
n ⋅ s = 0 n T × s = 0 n·s=0\\ n^T\times s =0 n⋅s=0nT×s=0
所以,为了使 n T × M ′ T × M × s = 0 n^T\times M'^T\times M\times s=0 nT×M′T×M×s=0成立,需要 M ′ T × M M'^T\times M M′T×M为单位矩阵 I I I,即:
M ′ T × M = I M'^T\times M=I M′T×M=I
可得:
M ′ = ( M − 1 ) T M'=(M^{-1})^T M′=(M−1)T
模型矩阵M包括了所有模型矩阵可以定义的变换方式,所以其逆转置矩阵适用于所有的模型矩阵变换的情况,来求解新的法向量。
当然,计算逆转置矩阵的操作对于计算机来说比较耗时,如果可以确定物体的变换只包括模型矩阵子矩阵可以解决的情况,直接使用其子矩阵更简便。
示例程序LightedTranslatedRotatedCube.js
相比于上一个示例,本示例除了添加模型矩阵之外,只需要对法向量进行矩阵变换即可。主要改动如下:
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_NormalMatrix;\n' + // 法向量变换矩阵
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightDirection;\n' + // 光线方向(归一化的世界坐标)
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行矩阵变换和归一化
' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
// 计算光线方向和法向量的点积
' float nDotL = max(dot(u_LightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
// 计算环境光产生的反射光颜色
' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
// 最终颜色
' v_Color = vec4(diffuse + ambient,a_Color.a);\n' +
'}\n'
// 模型视图投影矩阵
let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
if (!u_MvpMatrix) {
console.log('Failed to get the storage loaction of u_MvpMatrix')
return
}
let mvpMatrix = new Matrix4()
// 计算矩阵
// 模型矩阵
let modelMatrix = new Matrix4()
// 先旋转再平移
modelMatrix.setTranslate(0,1,0)
modelMatrix.rotate(-30,0,0,1)
// 投影和视图矩阵
mvpMatrix.setPerspective(30, 1, 1, 100)
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
mvpMatrix.multiply(modelMatrix)
// 矩阵传值
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)
// 法向量变换矩阵
let u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix')
if (!u_NormalMatrix) {
console.log('Failed to get the storage loaction of u_NormalMatrix')
return
}
let normalMatrix = new Matrix4()
normalMatrix.setInverseOf(modelMatrix) // 求逆
normalMatrix.transpose() // 转置
gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements)
此处用到了Matrix4对象的两种方法:setInverseOf()获得参数(也是Matrix4对象)的逆矩阵,transpose()对自身转置。
示例效果如下:
相关内容:1. 点光源光照效果实现:换了一个公式计算反射光,着色器负责反射光公式实现,JavaScript负责为着色器提供支持。2. 通过逐片元计算颜色(将计算颜色的过程放在片元着色器中,顶点着色器提供需要逐顶点计算的顶点坐标等内容,片元着色器使用内插出的坐标和法向量,结合输入的其它值计算反射光),可以有效避免颜色值线性内插造成的失真,其本质是点光源照射下颜色值在平面上的变化并非是线性的(笔者认为)。
如标题所示,这一部分讨论点光源光照效果的实现。
与平行光相比,点光源发出的光在三维空间的不同位置上方向不同。所以,要实现点光源光照效果,进行着色前需要在每个入射点计算点光源光的方向。
示例程序PointLightedCube.js
本节示例PointLightedCube.js
在本章第一节加入环境光的示例LightedCube_ambient,js
基础上改造而成,显示了一个点光源下的红色立方体。立方体表面依然是漫反射,环境光保持不变。点光源与平行光情况类似,只是换了一个公式计算反射光,着色器负责反射光计算,JavaScript负责为着色器提供支持。
因为改动了计算公式,示例对顶点着色器改动较多,加入了适合点光源的计算过程,JavaScript主要配合着色器进行改动,一些新的改动如下:
顶点着色器除了法向量等,新加入了模型矩阵、光源位置。此处单独输入模型矩阵,是为了根据模型矩阵获得变换后的顶点坐标,配合光源位置计算入射角。
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_ModelMatrix;\n' + // 模型矩阵
'uniform mat4 u_NormalMatrix;\n' + // 变换法向量的矩阵
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightPosition;\n' + // 光源位置(世界坐标系)
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行变换和归一化
' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
// 计算顶点的世界坐标
' vec4 vertexPosition = u_ModelMatrix * a_Position;\n' +
// 计算光线方向并归一化
' vec3 lightDirection = normalize(u_LightPosition - vec3(vertexPosition));\n' +
// 计算光线方向和法向量的点积 cosθ
' float nDotL = max(dot(lightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
// 计算环境光产生的反射光颜色
' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
// 最终颜色
' v_Color = vec4(diffuse + ambient,a_Color.a);\n' +
'}\n'
// 光线颜色
let u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor')
if (!u_LightColor) {
console.log('Failed to get the storage loaction of u_LightColor')
return
}
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0)
// 光源位置
let u_LightPosition = gl.getUniformLocation(gl.program,'u_LightPosition')
if (!u_LightPosition) {
console.log('Failed to get the storage loaction of u_LightPosition')
return
}
gl.uniform3f(u_LightPosition, 1.0, 1.5, 2.0)
// 模型矩阵
let u_ModelMatrix = gl.getUniformLocation(gl.program,'u_ModelMatrix')
if (!u_ModelMatrix) {
console.log('Failed to get the storage loaction of u_ModelMatrix')
return
}
let modelMatrix = new Matrix4()
modelMatrix.setRotate(90,0,1,0)
gl.uniformMatrix4fv(u_ModelMatrix,false,modelMatrix.elements)
// 变换法向量的矩阵
let u_NormalMatrix = gl.getUniformLocation(gl.program,'u_NormalMatrix')
if (!u_NormalMatrix) {
console.log('Failed to get the storage loaction of u_NormalMatrix')
return
}
let normalMatrix = new Matrix4()
normalMatrix.setInverseOf(modelMatrix)
normalMatrix.transpose()
gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements)
// 模型视图投影矩阵
let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
if (!u_MvpMatrix) {
console.log('Failed to get the storage loaction of u_MvpMatrix')
return
}
let mvpMatrix = new Matrix4()
// 计算矩阵
mvpMatrix.setPerspective(30, 1, 1, 100)
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
mvpMatrix.multiply(modelMatrix)
// 矩阵传值
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)
更逼真的方法:逐片元光照和PointLightedCube_perFragment.js
在上面的示例效果中,我们可以发现一些不自然的地方:在正方形面的对角线两侧有奇怪的颜色差异。这是因为在光栅化时,每个片元的颜色是根据顶点的颜色内插出的效果,虽然顶点的颜色是由点光源产生的,但实际情况下点光源照射到表面上的效果与简单使用4个顶点颜色内插出的效果并不完全相同,甚至有时差异很大。如果把示例换成球体,差异会更明显:(左图为逐顶点计算内插颜色的结果,右图是逐片元计算的结果)
笔者从数学的角度认为,造成逐顶点计算内插颜色出现错误的原因是:内插操作遵循的是线性内插,没有考虑顶点之间的颜色不是均匀变化的情况。而在实际情况下,两个顶点之间颜色变化与入射角的余弦值变化有关,依赖于顶点和光源的位置以及法向量的方向,单纯的余弦函数就不是直线而是曲线,如此一些信息叠加很难形成线性变化的结果。计算机图形学的大佬和数学大佬肯定有更细致的解释,待笔者之后考究。
所以,如果能够逐片元地计算颜色,就能在像素的尺度上避免这一问题。此时片元着色器终于能够摆脱只有几行代码的窘境。
示例实现也比较简单,只需要把计算反射光所需的内容通过varying变量传递给片元着色器,在着色器中计算反射光然后对gl_FragColor赋值即可。重要的改动如下:
将计算反射光需要的、在顶点着色器中接收或计算的逐顶点的内容:顶点世界坐标、法向量和基底色传递给片元着色器:
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' + // 模型视图投影矩阵
'uniform mat4 u_ModelMatrix;\n' + // 模型矩阵
'uniform mat4 u_NormalMatrix;\n' + // 变换法向量的矩阵
'varying vec4 v_Color;\n' +
'varying vec3 v_Normal;\n' +
'varying vec3 v_Position;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行变换和归一化
' v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
// 计算顶点的世界坐标
' v_Position = vec3(u_ModelMatrix * a_Position);\n' +
// 基底色
' v_Color = a_Color;\n' +
'}\n'
接收不随顶点变动的变量,包括光线颜色、光源位置(世界坐标系)、环境光颜色。
接收逐片元内插得到的片元世界坐标系坐标(原本是顶点,不使用gl_FragCoord是因为该坐标经过了视图和投影矩阵的改造)、法向量(最起码在立方体中,一个面法向量不变,法向量内插不会出现颜色内插的问题)、基底色。
进行相关颜色计算,赋值给gl_FragColor。
// 片元着色器
var FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightPosition;\n' + // 光源位置(世界坐标系)
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' + // 基底色
'varying vec3 v_Normal;\n' + // 法向量
'varying vec3 v_Position;\n' + // 片元位置(世界坐标系)
'void main(){\n' +
// 法线归一化(内插后长度不一定为1.0)
' vec3 normal = normalize(v_Normal);\n' +
// 计算光线方向并归一化
' vec3 lightDirection = normalize(u_LightPosition - v_Position);\n' +
// 计算光线方向和法向量的点积 cosθ
' float nDotL = max(dot(lightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * v_Color.rgb * nDotL;\n' +
// 计算环境光产生的反射光颜色
' vec3 ambient = u_AmbientLight * v_Color.rgb;\n' +
// 最终颜色
' gl_FragColor = vec4(diffuse+ambient,v_Color.a);\n' +
'}\n'
这样我们就可以得到一个视觉效果更好的立方体,二者对比如下,原始的逐顶点内插颜色的立方体为左图,本示例逐片元计算颜色的结果为右图:
书中小结如下:
这一章介绍了几种不同的光照类型和反射类型,讨论了如何为场景实现光照效果, 并基于这些知识实现了几种不同类型光源下的三维场景,探索了一些着色器的技巧,以增加效果的逼真程度。如你所见,掌握光照的技巧是很重要的。在正确的光照效果下, 三维场景会更加逼真,而缺了光照,它就会显得单调和枯燥。
笔者补充:
本章所言实现光照效果的技巧,实际上是了解了光照过程,建立数学模型(即相关公式)后,将公式的计算过程带入GLSL ES语言中,使着色器按照此模型计算颜色。本章包含的所有示例都遵循这一原则:着色器建立计算颜色的模型,JavaScript为该模型提供数据支持。
唯一的不同在于,WebGL系统中顶点着色器和片元着色器分属渲染流程的不同位置,在顶点着色器中计算和在片元着色器中(使用内插值)计算有不同的效果。