WebGL 着色器编译、链接与错误排查
一句话结论:着色器错误不会抛异常,只会让画面静默变黑,因此编译与链接的每一步都必须主动检查状态并读取日志。
着色器编译与链接流程
是什么
着色器编译与链接,是把 GLSL 源码变为可执行程序的四步流程:创建着色器对象并写入源码、编译、附加到程序对象、链接成 program。链接完成后才能查找 attribute 与 uniform 位置并开始绘制。
解决什么问题
它解决的是“GLSL 代码如何变成 GPU 可执行的机器指令”的问题。WebGL 把编译与链接拆成两步,使多个着色器可以共享同一个片元或顶点实现,也让错误定位更精确:编译报错定位到具体着色器,链接报错定位到两者接口不匹配。
为什么这样做
浏览器无法在页面运行时把 GLSL 编译成驱动指令,必须交给图形驱动完成,因此流程被设计成显式调用。链接阶段独立存在,是因为顶点与片元着色器的接口(varying 名称与类型)必须在校验期统一,驱动需要把两个阶段的符号表合并后统一优化。
怎么用
function createProgram(gl, vsSource, fsSource) {
const vs = compile(gl, gl.VERTEX_SHADER, vsSource);
const fs = compile(gl, gl.FRAGMENT_SHADER, fsSource);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error('链接失败: ' + log);
}
// 链接完成后着色器对象即可释放
gl.deleteShader(vs);
gl.deleteShader(fs);
return program;
}
function compile(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error((type === gl.VERTEX_SHADER ? '顶点' : '片元') + '着色器编译失败: ' + log);
}
return shader;
}
常见坑
- 只调用
linkProgram不检查状态 → 原因:误以为失败会抛异常 → 处理:每次编译与链接后都要读取状态参数 - 链接后忘记释放着色器对象 → 原因:资源占用累积 → 处理:链接成功后调用
deleteShader - 重复创建 program → 原因:在渲染循环内调用创建逻辑 → 处理:初始化阶段创建一次,循环内只使用
- 切换 program 后沿用旧的 attribute 位置 → 原因:location 是 program 级别的 → 处理:切换后重新查询并绑定
着色器错误信息读取
是什么
着色器错误信息读取,是通过 getShaderInfoLog、getProgramInfoLog 与 getError 获取失败原因的过程。WebGL 的错误信息由驱动生成,通常包含行号与错误类型,是定位 GLSL 问题的唯一可靠线索。
解决什么问题
它解决“画面黑屏但没有任何提示”的排查困境。由于 WebGL 出错不抛异常,日志是唯一能指明哪一行、哪类错误的输入。主动读取日志可以把排查时间从反复试错缩短到一次定位。
为什么这样做
错误信息只在着色器对象或 program 对象上保留,而编译失败时驱动内部状态已经不可用,因此必须在编译或链接之后立即读取,否则对象被删除后日志也就拿不到了。此外,运行期错误需要通过 getError 逐个清除,才能判断错误发生的位置。
怎么用
// 编译失败时打印带行号的日志,便于直接定位
function reportShaderError(gl, shader, source) {
const log = gl.getShaderInfoLog(shader);
console.error('[GLSL 编译失败]\n' + log);
source.split('\n').forEach((line, i) => console.error(String(i + 1).padStart(3) + ' | ' + line));
}
// 运行期错误:每次调用后检查并清空错误码
function checkGLError(gl, tag) {
const err = gl.getError();
if (err !== gl.NO_ERROR) {
console.warn('[WebGL 错误] ' + tag + ' 错误码: 0x' + err.toString(16));
}
}
常用错误码:INVALID_ENUM(参数枚举非法)、INVALID_VALUE(数值越界)、INVALID_OPERATION(状态不满足,如未绑定 program 就绘制)、OUT_OF_MEMORY(显存不足)。
常见坑
- 只打印日志不打印源码行 → 原因:日志行号与编辑器行号因拼接出错位 → 处理:同时输出源码行,用行号对齐
- 在循环里到处插
getError→ 原因:每次调用都同步 GPU,严重掉帧 → 处理:仅在开发模式按需检查 - 把 warning 当成功 → 原因:状态值判断写反 → 处理:统一用
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))处理失败分支 - 忽略
INVALID_OPERATION→ 原因:多由状态错误导致 → 处理:检查是否绑定 program、启用属性、绑定缓冲
要点总结
- 着色器流程为创建、编译、检查、附加、链接、再检查六步,缺一不可
- 编译与链接失败都不抛异常,必须主动读取状态与日志
- 链接成功后着色器对象即可删除,program 继续有效
- 运行期错误用
getError逐步定位,开发期启用,生产期关闭 - 日志要配合源码行一起打印,才能快速定位错误位置
