更新于 

光照实现

LightedCube

LightedCube Code
LightedCube运行效果
LightedCube运行效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let VSHADER_SOURCE = `
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;

// 对法向量进行归一化 vec4 → vec3
vec3 normal = normalize(vec3(a_Normal));
// 计算cosθ(光线方向·法向量)
float nDotL = max(dot(u_LightDirection,normal),0.0);
// 计算反射光颜色 (入射光颜色*基底颜色*cosθ )
vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;
v_Color = vec4(diffuse, 1.0);
}
`;

let FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_Color;
void main(){
gl_FragColor = v_Color;
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function main() {
let canvas = document.getElementById('webgl');
let gl = getWebGLContext(canvas);
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('failed to init shaders');
return;
}
let u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
let u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');

// 设置光线颜色-白色
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);
// 设置光线方向-世界坐标系下
// 0.25+9+16 = 25.25
let lightDirection = new Vector3([0.5, 3.0, 4.0]);
lightDirection.normalize(); // 归一化
gl.uniform3fv(u_LightDirection, lightDirection.elements);

let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
let mvpMatrix = new Matrix4();
mvpMatrix.setPerspective(
30, 1.0, 1.0, 100.0
).lookAt(
3, 3, 7, 0, 0, 0, 0, 1, 0
);
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

let n = initVertexBuffers(gl);
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, n, gl.UNSIGNED_BYTE, 0);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function initVertexBuffers(gl) {
let vertices = 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, // v0-v1-v2-v3 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, // v0-v3-v4-v5 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, // v0-v5-v6-v1 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, // v1-v6-v7-v2 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, // v7-v4-v3-v2 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 // v4-v7-v6-v5 back
]);
var colors = new Float32Array([ // Colors
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v1-v2-v3 front
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v3-v4-v5 right
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v5-v6-v1 up
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v1-v6-v7-v2 left
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v7-v4-v3-v2 down
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0  // v4-v7-v6-v5 back
]);
var normals = new Float32Array([ // Normal
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v0-v1-v2-v3 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, // v0-v3-v4-v5 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, // v0-v5-v6-v1 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, // v1-v6-v7-v2 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, // v7-v4-v3-v2 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 // v4-v7-v6-v5 back
]);
var indices = 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
]);
initArrayBuffer(gl, 'a_Position', vertices, 3, gl.FLOAT);
initArrayBuffer(gl, 'a_Color', colors, 3, gl.FLOAT);
initArrayBuffer(gl, 'a_Normal', normals, 3, gl.FLOAT);

let indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
return indices.length;
}
1
2
3
4
5
6
7
8
function initArrayBuffer(gl,attribute,data,num,type) {
let buffer = gl.createBuffer();
let a_Attribute = gl.getAttribLocation(gl.program, attribute);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Attribute, num, type, false, 0, 0);
gl.enableVertexAttribArray(a_Attribute);
}
一些小问题
入射光方向设置在世界坐标系下是什么意思?

答: 表示光照效果是在世界坐标系下计算的。
这样做可以使程序更简单,代码比较直观。

为什么在入射光方向归一化处理在Js中,法向量归一化处理在着色器内?

答:

  • 入射光方向 u_LightDirectionuniform 类型,适用于所有顶点,
    没有必要在着色器中逐顶点重复归一化操作,因此归一化后传入即可。
  • 法向量 a_Normalattribute 类型,放在缓冲区传入,
    每个顶点都有对应的法向量值,需要在着色器内逐顶点的处理。
法向量和入射光向量点积值小于0,说明出现了什么情况?

答: cosθ<0,说明θ>90°,即 光线照在了表面的背面上

Shader和Js中的normalize有什么区别?
Shader中
1
vec3 normal = normalize(vec3(a_Normal));

normalize为GLSL ES的内置函数,接受并返回 vec3 类型参数。

Js中
1
2
3
let lightDirection = new Vector3([0.5, 3.0, 4.0]);
lightDirection.normalize(); // 归一化
gl.uniform3fv(u_LightDirection, lightDirection.elements);

Vector3对象的normalize函数返回 Float32Array 类型参数,
并存储在Vector对象的 elements 属性中。

LightedCube_animation
LightedCube_animation运行效果
LightedCube_animation运行效果

注意在Cube顶点Position旋转变换时,也需要对顶点的法向量进行旋转变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal;

uniform mat4 u_MvpMatrix;
uniform mat4 u_NormalMatrix; // 法向量变换的模型矩阵
uniform vec3 u_LightColor;
uniform vec3 u_LightDirection;

varying vec4 v_Color;
void main(){
gl_Position = u_MvpMatrix * a_Position;
vec3 normal = vec3(u_NormalMatrix * a_Normal);
float nDotL = max(dot(normal,u_LightDirection),0.0);
vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;
v_Color = vec4(diffuse,1.0);
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
let u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
let u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
gl.uniform3f(u_LightColor,1.0, 1.0, 1.0);
let lightDirection = new Vector3([0.5, 3.0, 4.0]);
lightDirection.normalize();
gl.uniform3fv(u_LightDirection, lightDirection.elements);

let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
let u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
let mvMatrix = new Matrix4();
let normalMatrix = new Matrix4();
mvMatrix.setPerspective(
30, 1.0, 1.0, 100.0
).lookAt(
3, 3, 7, 0, 0, 0, 0, 1, 0
);
let modelMatrix = new Matrix4();
let mvpMatrix = new Matrix4();
let n = initVertexBuffers(gl);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);
let angle = 0;
let tick = function () {
// p * v * m
angle = animation(angle);
modelMatrix.setRotate(angle, 0, 1, 0);
mvpMatrix.set(mvMatrix).multiply(modelMatrix);
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
requestAnimationFrame(tick);
}
tick();

环境光下的漫反射

LightedCube存在的问题
Cube的右侧面几乎是黑色的
Cube的右侧面几乎是黑色的
实际光照下物体各面亮度差异不会这么大
实际光照下物体各面亮度差异不会这么大

LightedCube中使用的是 平行光源 ,没有被直射光照射到的面按理来说应该被 环境光 照亮。
由于环境光是由其他物体反射产生的,因此环境光的强度通常比较弱。

LightedCube_ambient
只有平行光的光照效果
只有平行光的光照效果
添加了环境光的光照效果
添加了环境光的光照效果
1
2
3
4
5
6
7
8
9
10
let VSHADER_SOURCE = `
...
uniform vec3 u_AmbientLight; //环境光颜色
void main{
...
// 计算环境光产生的反光颜色
vec3 ambient = u_AmbientLight * a_Color.rbg;
v_Color = vec4((diffuse + ambient), 1.0);
}
`;
1
2
let u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);

运动物体的光照效果

运动物体的法向量

当对物体进行变换时,需要考虑其法向量的变换情况。

  • 平移变换 不会改变法向量
  • 旋转变换 会改变法向量
  • 缩放变换 有可能会改变法向量,存在一些特殊情况,如:
    • 所有轴等比缩放
    • 向两个轴的方向等比缩放
魔法矩阵:逆转置矩阵

如何计算变换之后的法向量呢?
答: 将变换之前的法向量乘以 ModelMatrix的逆转置矩阵(inverse transpose matrix) 即可。

逆矩阵(inverse matrix)

如果矩阵M的逆矩阵是R,则 R*M=M*R=单位矩阵

1
2
3
4
5
let modelMatrix = new Matrix4(); //模型矩阵
let normalMatrix = new Matrix4(); //法向量变换矩阵
...
normalMatrix.setInverseOf(normalMatrix); // 求逆矩阵
normalMatrix.transpose(); //转置处理
LightedTranslatedRotatedCube
没有对法向量进行变换处理的效果
没有对法向量进行变换处理的效果
对法向量进行变换处理的效果
对法向量进行变换处理的效果
1
2
3
4
5
6
7
8
9
let VSHADER_SOURCE = `
...
uniform mat4 u_NormalMatrix; //法向量的模型矩阵
void main(){
...
vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));
...
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
let u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
let mvpMatrix = new Matrix4();
let modelMatrix = new Matrix4();
let normalMatrix = new Matrix4();
modelMatrix.setTranslate(
0,0.9,0
).rotate(
90, 0, 0, 1
);
mvpMatrix.setPerspective(
30, 1.0, 1.0, 100.0
).lookAt(
3, 3, 7, 0, 0, 0, 0, 1, 0
).multiply(modelMatrix);

normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();

gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);

点光源光

点光源光的方向

点光源光的方向不是 恒定不变 的,而是 根据每个顶点的位置 逐一计算的。

计算点光源光线方向需要哪些值呢?
回顾平行光源方向的获取
1
2
3
4
5
let VSHADER_SOURCE = `
...
uniform vec3 u_LightDirection; // 直接定义并从外界传入(世界坐标系下)
...
`

需要两个值:

  • 点光源位置 u_LightPosition
  • 顶点位置 a_Position
  • 计算顶点的世界坐标的模型矩阵 u_ModelMatrix
  • 顶点对应法向量的模型矩阵 u_NormalMatrix
如何计算每个顶点的点光源方向呢?
为什么要转换到世界坐标系下?

答: 你可能已经知道 u_MvpMatrix 是做什么用的了,
它会设置一个视点,并计算出传入的坐标 a_Position 相对视点的绘制坐标,

点光源光线是和 点光源和物体顶点的相对位置 有关的,即两个物体的相对位置关系
这个位置关系是 绝对的,与视点坐标系无关,

因此就需要一个 绝对的空间 来为它们做基准,这就是 世界坐标系

就像冬天你从寒冷的外面进入正常气温的屋子一样,你可能认为屋内气温比实际的要高一些,世界坐标系就像温度计一样,告诉你绝对现实是什么。
视点坐标:我相比之前感觉有了很大的进步,我正在逐渐理解一切!
世界坐标:哦?你是这样认为的吗?

Step1

计算顶点在 世界坐标系 中的坐标,
计算 世界顶点 相对应的 法向量

Step2

计算顶点处 点光源方向

设世界坐标系下,
顶点向量为 OA,点光源向量为 OB
则顶点处点光源光线向量为 BA = OA - OB
即 顶点世界矢量 - 点光源世界矢量
漫反射光的方向与入射光方向相反,
因此最终反射光线 = 点光源世界矢量 - 顶点世界矢量

PointLightedCube
PointLightedCube运行效果
PointLightedCube运行效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal;

uniform mat4 u_MvpMatrix;
uniform mat4 u_ModelMatrix;
uniform mat4 u_NormalMatrix;

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 = vec3(u_NormalMatrix * a_Normal); //计算法向量
vec3 vertexPosition = vec3(u_ModelMatrix * a_Position); //计算世界坐标
vec3 lightDirection = normalize( u_LightPosition - vertexPosition ); //计算光线方向
float nDotl = max(dot(lightDirection,normal),0.0); //计算cosθ
vec3 diffuse = u_LightColor * a_Color.rbg * nDotl; //漫反射
vec3 ambient = u_AmbientLight * a_Color.rbg; //环境反射
v_Color = vec4( diffuse + ambient , 1.0);
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function main() {
let canvas = document.getElementById('webgl');
let gl = getWebGLContext(canvas);
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('failed to init shaders');
return;
}

let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
let u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix');
let u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
let u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
let u_LightPosition = gl.getUniformLocation(gl.program, 'u_LightPosition');
let u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');

let mvpMatrix = new Matrix4();
let modelMatrix = new Matrix4();
let normalMatrix = new Matrix4();

modelMatrix.setRotate(90, 0, 1, 0);
mvpMatrix.setPerspective(
30, 1, 1, 100
).lookAt(
6, 6, 14, 0, 0, 0, 0, 1, 0
).multiply(modelMatrix);
normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();

gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
gl.uniform3f(u_LightPosition, 2.3, 4.0, 3.5);

let n = initVertexBuffers(gl);
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, n, gl.UNSIGNED_BYTE, 0);

}

逐片元光照

PointLightCube_animation

当物体运动时,会发现 立方体表面上有不自然的线条
这是片元的颜色是 由顶点颜色内插得出的 ,因此不够逼真。

逐片片元操作实际上就是把反射光颜色计算的步骤提取到片元着色器中。

PointLightedCube_perFragment

可以看到逐片元处理的物体表面颜色过渡更加自然。

逐顶点处理
逐顶点处理
逐片元处理
逐片元处理
一些小问题
将计算过程提取到片元着色器中,需要准备哪些数据?

答:

  • 片元世界坐标
  • 片元处法向量

把顶点坐标在顶点着色器中转换为世界坐标系下的坐标,将法向量进行相应转换,
这样通过varying传入的同名变量就已经是内插后的逐片元值了。

法向量传入片元着色器后为什么又要进行归一化处理?

答: 因为内插之后的法向量可能不再是1.0了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal;

uniform mat4 u_MvpMatrix;
uniform mat4 u_ModelMatrix;
uniform mat4 u_NormalMatrix;

varying vec3 v_Position;
varying vec3 v_Normal;
varying vec4 v_Color;
void main(){
gl_Position = u_MvpMatrix * a_Position;

v_Position = vec3(u_ModelMatrix * a_Position); //计算世界坐标
v_Normal = normalize(vec3(u_NormalMatrix * a_Normal)); //计算法向量
v_Color = a_Color;

}
`;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif

uniform vec3 u_LightColor; // 点光源颜色
uniform vec3 u_LightPosition; // 点光源位置
uniform vec3 u_AmbientLight; // 环境光颜色

varying vec3 v_Position;
varying vec3 v_Normal;
varying vec4 v_Color;
void main(){

// 需要重新对法线进行归一化处理,因为内插之后长度不一定是1.0
vec3 normal = normalize(v_Normal);
vec3 lightDirection = normalize( u_LightPosition - v_Position ); //计算光线方向
float nDotl = max(dot(lightDirection,normal),0.0); //计算cosθ
vec3 diffuse = u_LightColor * v_Color.rbg * nDotl; //漫反射
vec3 ambient = u_AmbientLight * v_Color.rbg; //环境反射
gl_FragColor = vec4( diffuse + ambient , 1.0);
}
`;
一些小实验
逐顶点绘制球体:表面颜色斑驳
逐顶点绘制球体:表面颜色斑驳
逐片元绘制球体:颜色过渡更自然
逐片元绘制球体:颜色过渡更自然