第 3 节:着色器编译与程序链接
🎯 学习目标
- 理解 Shader 的编译流程
- 掌握错误处理和调试技巧
- 理解 Program 的链接过程
- 对比原生 WebGL 和 Three.js 的 Shader 管理
📖 理论:Shader 编译流程
WebGL 中的 Shader 需要经过编译和链接才能使用:
Shader 源码 → 编译 → 链接 → Program → 使用
编译阶段
- 创建 Shader 对象:
gl.createShader(type) - 设置源码:
gl.shaderSource(shader, source) - 编译:
gl.compileShader(shader) - 检查错误:
gl.getShaderParameter(shader, gl.COMPILE_STATUS)
链接阶段
- 创建 Program:
gl.createProgram() - 附加 Shader:
gl.attachShader(program, shader) - 链接:
gl.linkProgram(program) - 检查错误:
gl.getProgramParameter(program, gl.LINK_STATUS)
常见错误类型
- 编译错误:语法错误、类型不匹配、未定义的变量
- 链接错误:varying 变量不匹配、main 函数缺失
💻 原生 WebGL 实现
让我们创建一个完整的 Shader 编译和错误处理系统:
<!DOCTYPE html>
<html>
<head>
<title>Shader 编译与错误处理</title>
<style>
body {
margin: 0;
font-family: monospace;
}
canvas {
display: block;
}
#error {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.8);
color: #ff0000;
padding: 10px;
max-width: 500px;
display: none;
}
</style>
</head>
<body>
<canvas id="glcanvas"></canvas>
<div id="error"></div>
<script>
const canvas = document.getElementById("glcanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const gl = canvas.getContext("webgl");
if (!gl) {
alert("无法初始化 WebGL");
}
// 完整的 Shader 编译函数(带错误处理)
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
// 检查编译状态
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const errorLog = gl.getShaderInfoLog(shader);
console.error(`Shader 编译错误 (${type === gl.VERTEX_SHADER ? "顶点" : "片段"}):`, errorLog);
// 显示错误信息
const errorDiv = document.getElementById("error");
errorDiv.textContent = `编译错误:\n${errorLog}`;
errorDiv.style.display = "block";
gl.deleteShader(shader);
return null;
}
return shader;
}
// 完整的 Program 链接函数(带错误处理)
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
// 检查链接状态
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const errorLog = gl.getProgramInfoLog(program);
console.error("Program 链接错误:", errorLog);
const errorDiv = document.getElementById("error");
errorDiv.textContent = `链接错误:\n${errorLog}`;
errorDiv.style.display = "block";
gl.deleteProgram(program);
return null;
}
// 验证 Program(可选,用于调试)
gl.validateProgram(program);
if (!gl.getProgramParameter(program, gl.VALIDATE_STATUS)) {
console.warn("Program 验证失败:", gl.getProgramInfoLog(program));
}
return program;
}
// 顶点着色器
const vertexShaderSource = `
attribute vec4 aVertexPosition;
attribute vec4 aVertexColor;
varying vec4 vColor;
void main() {
gl_Position = aVertexPosition;
vColor = aVertexColor;
}
`;
// 片段着色器
const fragmentShaderSource = `
precision mediump float;
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`;
// 编译和链接
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
if (!vertexShader || !fragmentShader) {
console.error("Shader 编译失败");
}
const program = createProgram(gl, vertexShader, fragmentShader);
if (!program) {
console.error("Program 链接失败");
} else {
gl.useProgram(program);
// 绘制三角形(复用上一节的代码)
const vertices = new Float32Array([
-0.5, -0.5, 0.0, 1.0, 0.0, 0.0,
0.5, -0.5, 0.0, 0.0, 1.0, 0.0,
0.0, 0.5, 0.0, 0.0, 0.0, 1.0,
]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const positionLoc = gl.getAttribLocation(program, "aVertexPosition");
const colorLoc = gl.getAttribLocation(program, "aVertexColor");
gl.enableVertexAttribArray(positionLoc);
gl.enableVertexAttribArray(colorLoc);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 6 * 4, 0);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 6 * 4, 3 * 4);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
</script>
</body>
</html>
错误处理技巧
- 编译时检查:每次编译后立即检查状态
- 详细错误信息:使用
getShaderInfoLog获取详细错误 - 验证 Program:使用
validateProgram检查 Program 是否可用 - 用户友好提示:将错误信息显示在页面上
🎨 Three.js 对比实现
Three.js 自动处理所有编译和链接过程:
<!DOCTYPE html>
<html>
<head>
<title>Three.js Shader 管理</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("canvas"),
});
renderer.setSize(window.innerWidth, window.innerHeight);
// Three.js 自动编译和链接 Shader
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array([
-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0,
]);
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
const colors = new Float32Array([
1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0,
]);
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
// Material 创建时自动编译 Shader
// 如果 Shader 编译失败,Three.js 会在控制台输出错误
const material = new THREE.MeshBasicMaterial({
vertexColors: true,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
renderer.render(scene, camera);
// Three.js 的错误处理
// 如果 Shader 编译失败,material 会使用默认的 Shader
// 错误信息会在控制台输出
</script>
</body>
</html>
Three.js 的 Shader 管理
- 自动编译:创建 Material 时自动编译 Shader
- 错误处理:编译失败时使用默认 Shader,错误输出到控制台
- Shader 缓存:相同参数的 Material 共享 Shader
- 调试支持:可以通过
material.onBeforeCompile修改 Shader
🔍 原理对比分析
1. 编译流程
原生 WebGL:
- 需要手动调用每个步骤
- 需要手动检查错误
- 完全控制编译过程
Three.js:
- 自动处理所有步骤
- 自动检查错误
- 隐藏了编译细节
2. 错误处理
原生 WebGL:
// 需要手动检查每个步骤
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
}
Three.js:
// 自动检查,错误输出到控制台
// 如果失败,使用默认 Shader
3. Shader 缓存
原生 WebGL:
- 需要手动管理 Shader 缓存
- 相同 Shader 需要重复编译
Three.js:
- 自动缓存相同参数的 Shader
- 提高性能,减少编译次数
📝 编程作业
基础作业(⭐)
任务:实现一个 Shader 编译工具
要求:
- 创建一个可以输入 Shader 代码的界面
- 实时编译和检查错误
- 显示编译结果和错误信息
检查清单:
- 可以输入顶点和片段着色器
- 实时显示编译错误
- 编译成功后显示渲染结果
进阶作业(⭐⭐)
任务:实现 Shader 热重载
要求:
- 监听文件变化或代码输入
- 自动重新编译 Shader
- 保持场景状态,只更新 Shader
检查清单:
- 可以实时更新 Shader
- 编译失败时不影响现有场景
- 显示编译状态(成功/失败)
挑战作业(⭐⭐⭐)
任务:创建 Shader 调试工具
要求:
- 高亮显示 Shader 代码中的错误行
- 显示变量绑定信息
- 显示 Uniform 和 Attribute 的使用情况
扩展功能:
- 支持语法高亮
- 支持代码补全
- 支持性能分析
🎓 本节总结
关键概念
- Shader 编译:源码 → 编译 → 检查错误
- Program 链接:Shader → 链接 → 检查错误
- 错误处理:使用
getShaderInfoLog和getProgramInfoLog - 验证:使用
validateProgram检查 Program
原生 WebGL vs Three.js
| 方面 | 原生 WebGL | Three.js |
|---|---|---|
| 编译流程 | 手动调用 | 自动处理 |
| 错误处理 | 手动检查 | 自动检查 |
| Shader 缓存 | 手动管理 | 自动缓存 |
| 调试支持 | 完全控制 | 有限支持 |
最佳实践
- 总是检查错误:编译和链接后立即检查
- 提供用户反馈:错误信息要清晰
- 使用验证:在开发时使用
validateProgram - 缓存 Shader:避免重复编译相同 Shader
下一步
完成作业后,进入下一节:第 4 节:Uniform 变量与动画