Skip to main content

第 3 节:着色器编译与程序链接

🎯 学习目标

  • 理解 Shader 的编译流程
  • 掌握错误处理和调试技巧
  • 理解 Program 的链接过程
  • 对比原生 WebGL 和 Three.js 的 Shader 管理

📖 理论:Shader 编译流程

WebGL 中的 Shader 需要经过编译和链接才能使用:

Shader 源码 → 编译 → 链接 → Program → 使用

编译阶段

  1. 创建 Shader 对象gl.createShader(type)
  2. 设置源码gl.shaderSource(shader, source)
  3. 编译gl.compileShader(shader)
  4. 检查错误gl.getShaderParameter(shader, gl.COMPILE_STATUS)

链接阶段

  1. 创建 Programgl.createProgram()
  2. 附加 Shadergl.attachShader(program, shader)
  3. 链接gl.linkProgram(program)
  4. 检查错误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>

错误处理技巧

  1. 编译时检查:每次编译后立即检查状态
  2. 详细错误信息:使用 getShaderInfoLog 获取详细错误
  3. 验证 Program:使用 validateProgram 检查 Program 是否可用
  4. 用户友好提示:将错误信息显示在页面上

🎨 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 管理

  1. 自动编译:创建 Material 时自动编译 Shader
  2. 错误处理:编译失败时使用默认 Shader,错误输出到控制台
  3. Shader 缓存:相同参数的 Material 共享 Shader
  4. 调试支持:可以通过 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 编译工具

要求

  1. 创建一个可以输入 Shader 代码的界面
  2. 实时编译和检查错误
  3. 显示编译结果和错误信息

检查清单

  • 可以输入顶点和片段着色器
  • 实时显示编译错误
  • 编译成功后显示渲染结果

进阶作业(⭐⭐)

任务:实现 Shader 热重载

要求

  1. 监听文件变化或代码输入
  2. 自动重新编译 Shader
  3. 保持场景状态,只更新 Shader

检查清单

  • 可以实时更新 Shader
  • 编译失败时不影响现有场景
  • 显示编译状态(成功/失败)

挑战作业(⭐⭐⭐)

任务:创建 Shader 调试工具

要求

  1. 高亮显示 Shader 代码中的错误行
  2. 显示变量绑定信息
  3. 显示 Uniform 和 Attribute 的使用情况

扩展功能

  • 支持语法高亮
  • 支持代码补全
  • 支持性能分析

🎓 本节总结

关键概念

  1. Shader 编译:源码 → 编译 → 检查错误
  2. Program 链接:Shader → 链接 → 检查错误
  3. 错误处理:使用 getShaderInfoLoggetProgramInfoLog
  4. 验证:使用 validateProgram 检查 Program

原生 WebGL vs Three.js

方面原生 WebGLThree.js
编译流程手动调用自动处理
错误处理手动检查自动检查
Shader 缓存手动管理自动缓存
调试支持完全控制有限支持

最佳实践

  1. 总是检查错误:编译和链接后立即检查
  2. 提供用户反馈:错误信息要清晰
  3. 使用验证:在开发时使用 validateProgram
  4. 缓存 Shader:避免重复编译相同 Shader

下一步

完成作业后,进入下一节:第 4 节:Uniform 变量与动画


📚 参考资源