这一章我们来讲解webgl中的光照概念,我们来先讲解一下平行光源,接下来的文章讨论点光源和聚光灯。
平行光源和法向量
假如没有光照,我们就什么都看不到,在不考虑反射的情况下,我们认为物体是漫发射,光线垂直照射的时候,我们应该看到最亮的。为了简化我们认为射进来的是白光,先不考虑颜色的叠加效应,效果大概是下面这个样子:
其实就是光线的方向和平面法线方向的余弦值,然后再乘以当前的颜色值,这样就有了光照。
那么什么是法线呢?如图
法线方向就是和物体表面切面垂直的方向。而法向量就是在法线方向上的单位向量。即在3D坐标系中长度为1的向量,
片元着色器
根据上面的内容,我们来介绍一下片元着色器:
precision mediump float;
// Passed in from the vertex shader.
varying vec3 v_normal;
uniform vec3 u_reverseLightDirection;
uniform vec4 u_color;
void main() {
// because v_normal is a varying it's interpolated
// so it will not be a unit vector. Normalizing it
// will make it a unit vector again
vec3 normal = normalize(v_normal);
float light = dot(normal, u_reverseLightDirection);
gl_FragColor = u_color;
// Lets multiply just the color portion (not the alpha)
// by the light
gl_FragColor.rgb *= light;
}
我们很容易看出,片元着色器最后取的值就是:当前颜色的rgb值乘以两个单位向量夹角的余弦值。
顶点着色器
顶点着色器变化不大:
// an attribute will receive data from a buffer
attribute vec3 a_position;
attribute vec3 a_normal;
uniform vec3 u_resolution;
uniform mat4 matrix_projection;
uniform mat4 matrix_translate;
varying vec3 v_normal;
void main() {
// gl_Position is a special variable a vertex shader
vec3 position = (matrix_translate * vec4(a_position, 1)).xyz;
// vec3 pro_position = (matrix_projection * vec4(position, 1)).xyz;
vec3 zeroToOne = position - u_resolution / 2.0;
// 再把 0->1 转换 0->2
vec3 zeroToTwo = zeroToOne / u_resolution * 2.0;
// 把 0->2 转换到 -1->+1 (裁剪空间)
vec3 clipSpace = zeroToTwo * vec3(1.0,-1.0,-1.0);
v_normal = a_normal;
gl_Position = vec4(clipSpace, 1);
}
这里需要把矩阵再优化一下。从坐标空间到裁剪空间需要使用矩阵变化。
这里再回忆一下Shader 中变量的类型与作用
-
attribute 变量:
- 只能在顶点着色器中使用
- 表示每个顶点特有的数据(如位置、法线、纹理坐标等)
- 由JavaScript通过
gl.vertexAttribPointer()传入
-
uniform 变量:
- 在顶点和片元着色器中都可使用
- 表示所有顶点/片元共用的数据(如变换矩阵、光源位置、颜色等)
- 由JavaScript通过
gl.uniformXXX()系列函数传入
-
varying 变量:
- 用于从顶点着色器向片元着色器传递数据
- 在顶点着色器中赋值,在片元着色器中读取
- 数据会在光栅化过程中被插值
还有webgl中的数据类型:
** 顶点属性数据类型(Vertex Attribute Types)**
这些类型用于 gl.vertexAttribPointer(type, size, normalized, stride, offset) 的 type 参数,指定顶点缓冲区中数据的存储格式。
| 数据类型常量 | 字节大小 | 范围或描述 | 典型用途 |
|---|---|---|---|
gl.BYTE | 1 | 有符号字节 [-128, 127] | 顶点坐标(整数) |
gl.UNSIGNED_BYTE | 1 | 无符号字节 [0, 255] | 颜色(RGB/A) |
gl.SHORT | 2 | 有符号短整型 [-32768, 32767] | 顶点坐标 |
gl.UNSIGNED_SHORT | 2 | 无符号短整型 [0, 65535] | 索引缓冲区(gl.ELEMENT_ARRAY_BUFFER) |
gl.INT | 4 | 有符号整型 [-2³¹, 2³¹-1] | 特殊用途(较少使用) |
gl.UNSIGNED_INT | 4 | 无符号整型 [0, 2³²-1] | 索引缓冲区(大型模型) |
gl.FLOAT | 4 | 32位浮点数 | 顶点坐标、法线、UV |
gl.HALF_FLOAT (WebGL2) | 2 | 16位半精度浮点数 | 节省内存的顶点数据 |
为什么要回忆这些?我在写代码的过程中出了点错,非常难排查。结果发现是数据类型问题,这里引用记录一下。
字母法向量
我们可以使用deepseek帮我们生成一下,当前顶点坐标的法向量。例如我的法向量是:
var vetrilNormal = [ // ===== 前面的 E (z=500) ===== // 横线(顶部) - 正面法向量 (0, 0, 1) ...Array(6).fill([0, 0, 1]).flat(),
// 竖线(左侧) - 正面法向量 (0, 0, 1)
...Array(6).fill([0, 0, 1]).flat(),
// 横线(底部) - 正面法向量 (0, 0, 1)
...Array(6).fill([0, 0, 1]).flat(),
// 中间横线 - 正面法向量 (0, 0, 1)
...Array(6).fill([0, 0, 1]).flat(),
// ===== 后面的 E (z=400) =====
// 横线(顶部) - 背面法向量 (0, 0, -1)
...Array(6).fill([0, 0, -1]).flat(),
// 竖线(左侧) - 背面法向量 (0, 0, -1)
...Array(6).fill([0, 0, -1]).flat(),
// 横线(底部) - 背面法向量 (0, 0, -1)
...Array(6).fill([0, 0, -1]).flat(),
// 中间横线 - 背面法向量 (0, 0, -1)
...Array(6).fill([0, 0, -1]).flat(),
// ===== 侧面(连接前后 E)=====
// 顶部横线侧面 - 上表面法向量 (0, 1, 0)
...Array(6).fill([0, 1, 0]).flat(),
// 顶部横线前侧面 - 右侧法向量 (1, 0, 0)
...Array(6).fill([1, 0, 0]).flat(),
// 顶部横线后侧面 - 左侧法向量 (-1, 0, 0)
...Array(6).fill([-1, 0, 0]).flat(),
// 顶部横线下侧面 - 下表面法向量 (0, -1, 0)
...Array(6).fill([0, -1, 0]).flat(),
// 左侧竖线侧面 - 左侧法向量 (-1, 0, 0)
...Array(6).fill([-1, 0, 0]).flat(),
// 左侧竖线右侧面 - 右侧法向量 (1, 0, 0)
...Array(6).fill([1, 0, 0]).flat(),
// 左侧竖线前侧面 - 前表面法向量 (0, 0, 1)
...Array(6).fill([0, 0, 1]).flat(),
// 左侧竖线后侧面 - 后表面法向量 (0, 0, -1)
...Array(6).fill([0, 0, -1]).flat(),
// 底部横线侧面 - 下表面法向量 (0, -1, 0)
...Array(12).fill([0, -1, 0]).flat(),
// 中间横线侧面 - 前表面法向量 (0, 0, 1) 和后表面法向量 (0, 0, -1)
...Array(6).fill([0, 0, 1]).flat(),
...Array(6).fill([0, 0, -1]).flat(),
// 中间横线右侧面 - 右侧法向量 (1, 0, 0)
...Array(6).fill([1, 0, 0]).flat()
];
有了法向量接下来的事情就简单了。
属性赋值
有了这些素材,接下来就简单了,对属性进行赋值就行了。
颜色赋值
var colorLocation = gl.getUniformLocation(program, "u_color");
gl.uniform4fv(colorLocation, [0.2, 1, 0.2, 1]); // green
光照赋值
var reverseLightDirectionLocation =
gl.getUniformLocation(program, "u_reverseLightDirection");
let lightDirection = vec3.create(); // 先创建向量
vec3.normalize(lightDirection, [1, 1, 0.5]); // 然后归一化
console.log('lightDirection',lightDirection);
gl.uniform3fv(reverseLightDirectionLocation, lightDirection);
法向量赋值
// 创建缓冲存储法向量
var normalBuffer = gl.createBuffer();
// 绑定到 ARRAY_BUFFER (可以看作 ARRAY_BUFFER = normalBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vetrilNormal), gl.STATIC_DRAW);
var normalLocation = gl.getAttribLocation(program, "a_normal");
// Turn on the normal attribute
gl.enableVertexAttribArray(normalLocation);
// Tell the attribute how to get data out of normalBuffer (ARRAY_BUFFER)
var v_size = 3; // 3 components per iteration
var v_type = gl.FLOAT; // the data is 32bit floating point values
var v_normalize = false; // normalize the data (convert from 0-255 to 0-1)
var v_stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var v_offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
normalLocation, v_size, v_type, v_normalize, v_stride, v_offset);
然后就是用原来的代码展示出来,
这已经看到了明显的光照效果。
代码地址:
gitee.com/feng-lianxi…
可是你运行以后会发现一个问题:当物体旋转的时候,光照似乎跟着旋转,不太像是平行光源。我们需要把这个问题处理掉。这究竟是怎么回事呢?根本原因是在字母旋转的过程中,我们的法向量并没有和字母一起旋转,所以当字母旋转的时候我们对法向量一起旋转就行了。 代码地址: gitee.com/feng-lianxi…