前期准备
先使用之前所学的知识渲染一个纯色立方体如下:
由于各个面都是同样的灰色,所以想要看出它是个立方体,需要一定的想象力的加持。在 c4d 等构建三维物体的软件里,我们是可以添加灯光对象的,从而让物体表面呈现出明暗对比,让人一眼看出物体的形状:
在 webgl 中,我们也可以给物体添加光照,并且请注意,在现实世界中,我们之所以能看到物体,是因为物体被光线照射后反射了一部分光进入了人的眼睛。所以我们要给目标物体添加光照的效果,要考虑 2 大部分 ------ 光源和反射。
光源
先来分析光源,根据光源发出的光线方向的不同,我们可以将光源分为 2 种:
平行光(directional light)
光线互相平行,射向同一个方向,如太阳光。定义时只需要光线的方向和光的颜色,至于光源的位置,可以认为是特别远的地方,无需定义。
点光源(point light)
本案例中要使用的就是点光源,它的光线从一点向周围放射,比如灯泡。定义点光源时,需要定义光源的颜色、位置和光线的方向。我们可以在顶点着色器源码的 main
函数中,定义一个类型为 vec3
的变量 pointLightColor
代表点光源的颜色;另外定义一个类型为 vec3
的变量 pointLightPosition
代表点光源的位置:
javascript
// 代码片段 1.1
const vsSource = `
void main() {
// 定义点光源的颜色 - 红色
vec3 pointLightColor = vec3(1.0, 0.0, 0.0);
// 定义点光源的位置
vec3 pointLightPosition = vec3(10.0, 10.0, 10.0);
}
`
至于点光源的光线方向 lightDirect
,则需要根据光源和物体的位置进行计算,其实就是做个向量的减法:
注意,这里说的光线的方向其实是光源入射方向的反方向,是用光源的位置 pointLightPosition
减去物体的顶点坐标 vPosition
,然后用 GLSL 的内置函数 normalize
进行归一化处理得到的:
javascript
// 代码片段 1.2
const vsSource = `
void main() {
// 点的坐标
vec4 vPosition = uMatrix * aPosition;
// 光线方向
vec3 lightDirect = normalize(pointLightPosition - vec3(vPosition));
}
`
环境光(ambient light)
除了平行光和点光源,还有一种间接光,是光源发出的光线照射到环境中的各个物体后产生的反射。环境光的光线会均匀地照射到目标对象表面,其强度差距很小,无需精确计算光线强度,只需定义光的颜色即可:
javascript
// 代码片段 1.3
const vsSource = `
void main() {
// 定义环境光的颜色
vec3 ambientLightColor = vec3(0.2, 0.0, 0.0);
}
`
反射
现在来分析光的反射,反射可以大致分为以下 3 类:
环境反射(environment reflection)
先是最容易定义其反射颜色的环境反射。物体对环境光的反射就是环境反射,反射的方向为入射光的反方向。环境反射的颜色 = 环境光的颜色 * 物体表面的颜色:
javascript
// 代码片段 2.1
const vsSource = `
void main() {
// 物体表面的颜色 - 灰色
vec4 aColor = vec4(0.8, 0.8, 0.8, 1.0);
// 环境反射的颜色
vec3 ambientColor = ambientLightColor * vec3(aColor);
}
`
漫反射(diffuse reflection)
接着是现实生活中最常见的漫反射。因为大多数物体的表面都是粗糙不平的,所以在接收到光源的入射光后,反射的光线会均匀地射向四周,称为漫反射。漫反射的反射光的颜色 =入射光的颜色 * 物体表面的颜色 * cosα。
javascript
// 代码片段 2.2
const vsSource = `
void main() {
// 漫反射光的颜色
vec3 diffuseColor = pointLightColor * vec3(aColor) * deg;
}
`
入射角
代码片段 2.2 中的 deg 为入射光与物体表面的法向量形成的入射角 α 的余弦值:
由下图易知,在同样的光源下,在 0 -90°的区间内,入射角越小,则物体表面接收到的光线数量就越大:
所以在求漫反射的反射光颜色的公式中,就用入射角的余弦值 cosα
来表示物体的受光程度。 因为 l · n = |l| * |n| * cosα
,l
和 n
为单位向量,则 |l|
和 |n|
均为 1
,所以 cosα = l · n
。注意,向量的点积是符合乘法交换律的:
javascript
// 代码片段 2.3
const vsSource = `
attribute vec4 aNormal;
void main() {
// 入射角
float deg = dot(lightDirect, vec3(aNormal));
}
`
lightDirect
为代码片段 1.2 中定义的光线方向,aNormal
则为各个顶点的法向量。
法向量
aNormal
为 attribute 变量,我们可以定义好立方体 6 个面的法向量,赋值给变量 normals
中,然后将 normals
存入缓冲区对象让 aNormal
去读取:
javascript
// 代码片段 2.4
const aNormal = gl.getAttribLocation(program, 'aNormal')
// 法向量
const 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,
// 顶面
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,
// 左侧面
-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, 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
])
// 创建缓冲区对象
const normalBuffer = gl.createBuffer()
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer)
// 将数据存入缓冲区对象
gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW)
gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(aNormal)
注意,因为在创建顶点数据 points
时每个面都有 4 个顶点数据,所以 normals
中每个面也需要对应的建立 4 次法线数据。
镜面反射(specular reflection)
最后一种是镜面反射。物体接收到光源的入射光后,会将光线以与物体表面法线对称的方向反射出去。
物体的最终颜色
当同时存在漫反射和环境反射时,物体的最终颜色就应该是漫反射光颜色 + 环境反射光颜色:
javascript
// 代码片段 3.1
const vsSource = `
vColor = vec4(ambientColor + diffuseColor, aColor.a);
`
注意,代码片段 2.1 获取的 ambientColor
和代码片段 2.2 获取的 diffuseColor
都是 vec3
类型的,而最终赋给 gl_FragColor
的值需要是 vec4
类型的,所以使用了矢量构造函数 vec4()
,代表透明度的参数则传入物体表面原本的颜色 aColor
的 a
分量。
最终效果如下,可以看到添加了红光后物体,有了明暗效果,一眼看出是个立方体: