光照原理
现实世界中的物体被光线照射时,会反射一部分光,只有当反射光线进人眼睛时,才能够看到物体并辩认出它的颜色
在现实世界中,当光线照射到物体上时,发生了两个重要的现象:
- 根据光源和光线方向,物体不同表面的明暗程度变得不一致(给了物体立体感)
- 根据光源和光线方向,物体向地面投下了影子
在三维图形学中,着色器最初被发明出来就是为了重建光照产生的明暗现象,称为着色(shading)。而物体向地面投下影子的现象,称为阴影(shadowing)
本节介绍着色,而阴影在下章介绍
光源类型
- 平行光:平行光具有方向,光线相互平行,可以看作是无限远处的光源(比如太阳)发出的光
- 可以用一个方向和一个颜色来定义
- 点光源光:从一个点向周围的所有方向发出的光,可以表示现实中的灯泡、火焰等
- 需要指定点光源的位置和颜色
- 光线的方向将根据点光源的位置和被照射之处的位置计算出来
- 环境光:是指那些经光源(点光源或平行光源)发出后,被墙壁等物体多次反射,然后照到物体表面上的光
- 环境光从各个角度照射物体,其强度都是一致的(实际上,环境光是各种光被各种表面多次反射后形成的。我们认为环境光是“均匀”照射到物体表面上的,因为没有必要去精确计算环境光的产生过程)
- 环境光不用指定位置和方向,只需要指定颜色即可
三维图形学还使用一些其他类型的光,比如用聚光灯光来模拟电筒、车前灯等
反射类型
物体向哪个方向反射光,反射的光是什么颜色,取决于以下两个因素:
- 入射光,包括:入射光的方向和颜色
- 物体表面的类型,包括:固有颜色(也称基底色)和反射特性
物体表面反射光线的方式有两种:漫反射(diffuse reflection)和环境反射(enviroment/ambient reflection)
本节的重点是如何根据入射光和物体表面特性来计算出反射光的颜色,会涉及一些简单的数学计算
漫反射
漫反射是针对平行光或点光源而言的,漫反射的反射光在各个方向上是均匀的
为什么漫反射的反射光在各个方向上是均匀的?
如果物体表面像镜子一样光滑,那么光线就会以特定的角度反射出去,但是现实中的大部分材质,比如纸张、岩石、塑料等,其表面都是粗糙的,在这种情况下反射光就会以不固定的角度反射出去,漫反射就是针对后一种情况而建立的理想反射模型
在漫反射中,反射光的颜色取决于入射光的颜色、表面的基底色、入射光与表面形成的入射角。将入射角定义为入射光与表面的法线形成的夹角,并用 表示,那么漫反射光的颜色可以根据下式计算得到:
乘法操作是在颜色矢量上逐分量(R、G、B)进行的。而且因为漫反射光在各个方向上都是“均匀”的,所以从任何角度看上去其强度都相等
根据光线和表面的方向计算入射角
通过计算入射光的方向和物体表面的朝向(即法线方向)这两个矢量的点积,来计算这两个矢量的夹角余弦值
对矢量 和 作点积运算,公式是:,其中 符号表示取矢量的长度。可见,如果两个矢量的长度都是 1.0,那么其点积的结果就是 值
GLSL ES 内置了点积运算函数,实际上是用的下面这个公式: 为 (nx, ny, nz), 为 (lx, ly, lz),那么 ,该公式可由余弦定理得到
所以漫反射光的颜色公式改写成:
这里有两点需要注意:其一,光线方向矢量和表面法线矢量的长度必须为 1,否则反射光的颜色就会过暗或过亮。将一个矢量的长度调整为 1,同时保持方向不变的过程称之为归一化(normalization)。GLSLES 提供了内置的归一化函数,可以直接使用
比如矢量 为 (nx, ny, nz),则其长度为 ,对其进行归一化后的结果是 (nx/m, ny/m, nz/m),其中 m 为 n 的长度
其二,所谓的“光线方向”,实际上是入射方向的反方向,即从入射点指向光源方向。因为这样,该方向与法线方向的夹角才是入射角
法线:表面的朝向
物体表面的朝向,即垂直于表面的方向,称为法线或法向量
一个表面具有两个法向量:一个表面有正面和反面,两个面各自具有一个法向量
在三维图形学中,表面的正面和背面取决于绘制表面时的顶点顺序。当按照 v0、v1、v2、v3 的顶点顺序绘制一个平面,从正面观察这个表面时,这 4 个顶点是顺时针的,从背面观察该表面,这 4 个项点就是逆时针的(“右手法则”)
平面的法向量唯一:由于法向量表示的是方向,与位置无关,所以一个平面只有一个法向量
换句话说,平面的任意一点都具有相同的法向量。进一步来说,即使有两个不同的平面,只要其朝向相同(也就是两个平面平行),法向量也相同
环境反射
环境反射是针对环境光而言的。在环境反射中,反射光的方向可以认为就是入射光的反方向。由于环境光照射物体的方式就是各方向均匀、强度相等的,所以反射光也是各向均匀的,其颜色为:
当漫反射和环境反射同时存在时,将两者加起来,就会得到物体最终被观察到的颜色:
注意,两种反射光并不一定总是存在,也并不一定要完全按照上述公式来计算。渲染三维模型时,可以修改这些公式以达到想要的效果
物体的光照效果
平行光下的漫反射
立方体及每个表面的法向量如下:
每个顶点对应 3 个法向量,就像之前每个顶点都对应 3 个颜色值一样,这是因为立方体比较特殊,各表面垂直相交,所以每个顶点供三个面使用(在缓冲区中被拆成 3 个顶点)。但是,一些表面光滑的物体,通常其每个顶点只对应 1 个法向量、1 个颜色值
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal; // 法向量
uniform mat4 u_MvpMatrix;
uniform vec3 u_LightColor; // 光线颜色
uniform vec3 u_LightDirection; // 光照方向(归一化的世界坐标)
varying vec4 v_Color;
void main() {
gl_Position = u_MvpMatrix * a_Position;
// 对法向量进行归一化
vec3 normal = normalize(a_Normal.xyz);
// 计算光线方向和法向量的点积
float nDotL = max(dot(u_LightDirection, normal), 0.0);
// 计算漫反射光的颜色
vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;
v_Color = vec4(diffuse, a_Color.a);
}
`
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
`
function main() {
// 获取 canvas 元素
const canvas = document.getElementById('canvas')
// 获取 WebGL 上下文
const gl = canvas.getContext('webgl') // 'webgl'、'webgl2'
if (!gl) {
throw new Error('当前浏览器不支持 WebGL')
}
// 初始化着色器
if (!initShaders(gl, vertexShaderSource, fragmentShaderSource)) {
throw new Error('初始化着色器失败')
}
// 设置顶点坐标
const vertexData = new Float32Array([
1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, // front
1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, // right
1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, // up
-1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, // left
-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, // down
1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, // back
])
handleArrayBuffer(gl, vertexData, 'a_Position', 3)
// 设置顶点颜色(都是白色)
const colorData = new Float32Array(Array.from({ length: 3 * 4 * 6 }).fill(1))
handleArrayBuffer(gl, colorData, 'a_Color', 3)
// 设置顶点法向量
var normalData = 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, // front
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // right
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // up
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, // left
0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, // down
0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, // back
])
handleArrayBuffer(gl, normalData, 'a_Normal', 3)
// 设置顶点索引
const indicesData = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9, 10, 8, 10, 11, // up
12, 13, 14, 12, 14, 15, // left
16, 17, 18, 16, 18, 19, // down
20, 21, 22, 20, 22, 23, // back
])
handleIndexBuffer(gl, indicesData)
// 获取 uniform 变量
const u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
const u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor')
const u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection')
if (!u_MvpMatrix || !u_LightColor || !u_LightDirection) {
throw new Error('获取 uniform 变量失败')
}
// 设置 mvp 矩阵
const mvpMatrix = new Matrix4()
mvpMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100)
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)
// 设置光线颜色
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0) // 白色
// 设置光照方向(归一化的世界坐标)
const lightDirection = new Vector3([0.5, 3.0, 4.0])
lightDirection.normalize() // 归一化
gl.uniform3fv(u_LightDirection, lightDirection.elements)
// 设置清除颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 开启深度测试
gl.enable(gl.DEPTH_TEST)
// 清除颜色缓冲区和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
// 绘制图形(立方体)
gl.drawElements(gl.TRIANGLES, indicesData.length, gl.UNSIGNED_BYTE, 0)
}
main()
/**
* 创建 gl.ARRAY_BUFFER 缓冲区对象,并将数据保存在缓冲区中,然后将缓冲区传给顶点着色器 attribute 变量
* @param {*} gl webgl 上下文
* @param {*} data 数据
* @param {string} name attribute 变量名
* @param {number} size 缓冲区中每个顶点的分量个数
*/
function handleArrayBuffer(gl, data, name, size) {
// 创建缓冲区对象
const buffer = gl.createBuffer()
if (!buffer) {
throw new Error(`创建 '${name}' ARRAY_BUFFER 缓冲区对象失败`)
}
// 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
// 写入内容
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
// 获取 attribute 变量的存储位置
const attribute = gl.getAttribLocation(gl.program, name)
if (attribute < 0) {
throw new Error(`attribute 变量 '${name}' 位置获取失败`)
}
// 将缓冲区对象分配给 attribute 变量
gl.vertexAttribPointer(attribute, size, gl.FLOAT, false, 0, 0)
// 开启 attribute 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(attribute)
}
/**
* 创建 gl.ELEMENT_ARRAY_BUFFER 缓冲区对象,并将数据保存在缓冲区中
* @param {*} gl webgl 上下文
* @param {*} data 数据
*/
function handleIndexBuffer(gl, data) {
var buffer = gl.createBuffer()
if (!buffer) {
throw new Error(`创建 ELEMENT_ARRAY_BUFFER 缓冲区对象失败`)
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW)
}
使用了漫反射颜色计算公式
点积值小于 0,意味着入射角,也就是入射反方向(光线方向)与表面法向量的夹角,大于 90 度。说明光线照射在表面的背面上,此时,将 nDotL
赋为 0
加上环境反射
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal; // 法向量
uniform mat4 u_MvpMatrix;
uniform vec3 u_LightColor; // 光线颜色
uniform vec3 u_LightDirection; // 光照方向(归一化的世界坐标)
uniform vec3 u_AmbientLight; // 环境光颜色
varying vec4 v_Color;
void main() {
gl_Position = u_MvpMatrix * a_Position;
// 计算漫反射光的颜色
vec3 diffuse = u_LightColor * vec3(a_Color) * max(dot(u_LightDirection, normalize(a_Normal.xyz)), 0.0);
// 计算环境光的颜色
vec3 ambient = u_AmbientLight * vec3(a_Color);
// 最后计算光照颜色
v_Color = vec4(diffuse + ambient, a_Color.a);
}
`
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
`
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 设置顶点坐标、颜色、法向量、索引
// 获取 uniform 变量
// 设置 mvp 矩阵
// 设置光线颜色
// 设置光照方向(归一化的世界坐标)
// 设置环境光颜色
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2)
// 设置清除颜色
// 开启深度测试
// 清除颜色缓冲区和深度缓冲区
// 绘制图形(立方体)
如你所见,与只有平行光下的漫反射相比,本例加了环境反射,就使得整个立方体变亮了一些,这正是环境光从各个方向均匀照射在立方体上产生的
魔法矩阵:逆转置矩阵
物体的运动会改变每表面的法向量,从而导致光照效果发生变化
- 平移变换不会改变法向量,因为平移不会改变物体的方向
- 旋转变换会改变法向量,因为旋转改变了物体的方向
- 缩放变换对法向量的影响较为复杂
- 最右侧的图显示了立方体先旋转了 45 度,再在 y 轴上拉伸至原来的 2 倍的情况。此时法向量改变了,因为表面的朝向改变了
- 但是,如果缩放比例在所有的轴上都一致的话,那么法向量就不会变化
- 最后,即使物体在某些轴上的缩放比例并不一致,法向量也并不一定会变化,比如将最左侧图中的立方体在 y 轴方向上拉伸两倍,法向量就不会变化
显然,在对物体进行不同变换时,法向量的变化情况较为复杂(特别是缩放变换时)。这时候,数学公式就会派上用场了
对顶点进行变换的矩阵称为模型矩阵,如何计算变换之后的法向量呢?只要将变换之前的法向量乘以模型矩阵的逆转置矩阵(inverse transpose matrix)即可。所谓逆转置矩阵,就是逆矩阵的转置
- 逆矩阵:如果矩阵 M 的逆矩阵是 R,那么
R * M
或M * R
的结果都是单位矩阵 - 转置:矩阵的行列进行调换(看上去就像是沿着左上——右下对角线进行了翻转)
规则:用法向量乘以模型矩阵的逆转置矩阵,就可以求得变换后的法向量。求逆转值矩阵的两个步骤:
- 求原矩阵的逆矩阵:
Matrix4.setInverseOf(m)
,使自身成为矩阵 m 的逆矩阵 - 将上一步求得的逆矩阵进行转置:
Matrix4.transpose()
,使自身进行转置操作
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal; // 法向量
uniform mat4 u_NormalMatrix; // 用来变换法向量的矩阵
uniform mat4 u_MvpMatrix;
uniform vec3 u_LightColor; // 光线颜色
uniform vec3 u_LightDirection; // 光照方向(归一化的世界坐标)
uniform vec3 u_AmbientLight; // 环境光颜色
varying vec4 v_Color;
void main() {
gl_Position = u_MvpMatrix * a_Position;
// 计算变换后的法向量并归一化
vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));
// 计算光线方向和法向量的点积
float nDotL = max(dot(u_LightDirection, normal), 0.0);
// 计算漫反射光的颜色
vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;
// 计算环境光的颜色
vec3 ambient = u_AmbientLight * vec3(a_Color);
// 最后计算光照颜色
v_Color = vec4(diffuse + ambient, a_Color.a);
}
`
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
`
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 设置顶点坐标、颜色、法向量、索引
// 获取 uniform 变量
// 设置模型矩阵
const modelMatrix = new Matrix4()
modelMatrix.setTranslate(0, 1, 0) // 沿 Y 轴平移
modelMatrix.rotate(30, 0, 0, 1) // 绕 Z 轴旋转
modelMatrix.scale(0.7, 0.5, 1.2) // 缩放
// 设置 mvp 矩阵
const mvpMatrix = new Matrix4()
mvpMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100)
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
mvpMatrix.multiply(modelMatrix)
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)
// 设置用来变换法向量的矩阵
const normalMatrix = new Matrix4()
normalMatrix.setInverseOf(modelMatrix)
normalMatrix.transpose()
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements)
// 设置光线颜色
// 设置光照方向(归一化的世界坐标)
// 设置环境光颜色
// 设置清除颜色
// 开启深度测试
// 清除颜色缓冲区和深度缓冲区
// 绘制图形(立方体)
点光源光下的漫反射
与平行光相比,点光源光发出的光,在三维空间的不同位置上其方向也不同。所以,在对点光源光下的物体进行着色时,需要在每个入射点计算点光源光在该处的方向
平行光下的漫反射根据每个顶点的法向量和平行光入射方向来计算反射光的颜色,这里同理,只不过点光源光的方向不再是恒定不变的,而要根据每个顶点的位置逐一计算。着色器需要知道点光源光自身的所在位置,而不是光的方向
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal; // 法向量
uniform mat4 u_NormalMatrix; // 用来变换法向量的矩阵
uniform mat4 u_ModelMatrix; // 模型矩阵
uniform mat4 u_MvpMatrix;
uniform vec3 u_LightColor; // 光线颜色
uniform vec3 u_LightPosition; // 点光源位置(世界坐标)
uniform vec3 u_AmbientLight; // 环境光颜色
varying vec4 v_Color;
void main() {
gl_Position = u_MvpMatrix * a_Position;
// 计算变换后的法向量并归一化
vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));
// 计算顶点的世界坐标
vec4 vertexPosition = u_ModelMatrix * a_Position;
// 计算光线方向并归一化
vec3 lightDirection = normalize(u_LightPosition - vertexPosition.xyz);
// 计算光线方向和法向量的点积
float nDotL = max(dot(lightDirection, normal), 0.0);
// 计算漫反射光的颜色
vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;
// 计算环境光的颜色
vec3 ambient = u_AmbientLight * vec3(a_Color);
// 最后计算光照颜色
v_Color = vec4(diffuse + ambient, a_Color.a);
}
`
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
`
// 获取 canvas 元素
// 获取 WebGL 上下文
// 初始化着色器
// 设置顶点坐标、颜色、法向量、索引
// 获取 uniform 变量
// 设置模型矩阵
const modelMatrix = new Matrix4()
modelMatrix.rotate(-30, 0, 1, 0) // 绕 Y 轴旋转
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
// 设置 mvp 矩阵
const mvpMatrix = new Matrix4()
mvpMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100)
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
mvpMatrix.multiply(modelMatrix)
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)
// 设置用来变换法向量的矩阵
const normalMatrix = new Matrix4()
normalMatrix.setInverseOf(modelMatrix)
normalMatrix.transpose()
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements)
// 设置光线颜色
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0) // 白色
// 设置点光源位置(世界坐标)
gl.uniform3f(u_LightPosition, 0.0, 3.0, 4.0)
// 设置环境光颜色
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2)
// 设置清除颜色
// 开启深度测试
// 清除颜色缓冲区和深度缓冲区
// 绘制图形(立方体)
最关键的变化发生在顶点着色器中,首先使用模型矩阵变换顶点坐标,获得顶点在世界坐标系中的坐标(即变换后的坐标),以便计算点光源光在顶点处的方向。点光源向四周放射光线,所以顶点处的光线方向是由点光源光坐标减去顶点坐标而得到的矢量,需要归一化,以保证光线方向矢量的长度为 1.0
如果仔细观察上图中的立方体,能发现一个问题:立方体表面上有不自然的线条
出现该现象的原因是 WebGL 系统会根据顶点的颜色内插出表面上每个片元的颜色。实际上,点光源光照射到一个表面上,所产生的效果(即每个片元获得的颜色)与简单使用 4 个顶点颜色内插出的效果并不完全相同(在某些极端情况下甚至很不一样),所以为了使效果更加逼真,需要对表面的每一点计算光照效果
逐片元光照
// 顶点着色器程序(GLSL ES)
const vertexShaderSource = `
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal; // 法向量
uniform mat4 u_NormalMatrix; // 用来变换法向量的矩阵
uniform mat4 u_ModelMatrix; // 模型矩阵
uniform mat4 u_MvpMatrix;
varying vec4 v_Color;
varying vec3 v_Normal; // 变换后的法向量
varying vec3 v_Position; // 顶点的世界坐标
void main() {
gl_Position = u_MvpMatrix * a_Position;
v_Color = a_Color;
v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));
v_Position = vec3(u_ModelMatrix * a_Position);
}
`
// 片元着色器程序(GLSL ES)
const fragmentShaderSource = `
precision mediump float;
uniform vec3 u_LightColor; // 光线颜色
uniform vec3 u_LightPosition; // 点光源位置(世界坐标)
uniform vec3 u_AmbientLight; // 环境光颜色
varying vec4 v_Color;
varying vec3 v_Normal; // 变换后的法向量
varying vec3 v_Position; // 顶点的世界坐标
void main() {
// 重新归一化,因为其内插之后长度不一定为 1
vec3 normal = normalize(v_Normal);
// 计算光线方向并归一化
vec3 lightDirection = normalize(u_LightPosition - v_Position);
// 计算光线方向和法向量的点积
float nDotL = max(dot(lightDirection, normal), 0.0);
// 计算漫反射光的颜色
vec3 diffuse = u_LightColor * v_Color.rgb * nDotL;
// 计算环境光的颜色
vec3 ambient = u_AmbientLight * v_Color.rgb;
// 最后计算光照颜色
gl_FragColor = vec4(diffuse + ambient, v_Color.a);
}
`
就是把在顶点着色器逐顶点计算颜色后内插到片元着色器,改成在片元着色器逐片元计算颜色
如果场景中有超过一个光源(平行光、点光源光、环境光),那么就需要在片元着色器中计算每一个光源对片元的颜色贡献,并将它们全部加起来