{T}

WebGL 快速入门

环境准备

浏览器要求

WebGL 需要支持 WebGL API 的现代浏览器:

浏览器WebGL 1.0WebGL 2.0推荐版本
Chrome✅ 支持✅ 支持56+
Firefox✅ 支持✅ 支持51+
Safari✅ 支持✅ 支持15+
Edge✅ 支持✅ 支持17+

开发工具

推荐使用以下工具提升开发体验:

  • Chrome DevTools:内置 WebGL 调试,按 F12 打开
  • WebGL Inspector:浏览器扩展,深度分析 WebGL 调用
  • Spector.js:扩展工具,记录每一帧的 WebGL 状态

创建项目结构

bash
webgl-demo/
├── index.html          # 主页面
├── main.js            # WebGL 主程序
└── shaders/
    ├── vertex.glsl    # 顶点着色器
    └── fragment.glsl  # 片段着色器

第一个 WebGL 程序

1. 创建 Canvas 元素

创建 index.html 文件:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>第一个 WebGL 程序</title>
  <style>
    canvas {
      border: 1px solid #ccc;
      display: block;
      margin: 20px auto;
    }
  </style>
</head>
<body>
  <canvas id="glCanvas" width="800" height="600"></canvas>
  <script src="main.js"></script>
</body>
</html>

2. 获取 WebGL 上下文

创建 main.js 文件:

javascript
// 获取 Canvas 元素
const canvas = document.getElementById('glCanvas');

// 获取 WebGL 上下文
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

// 检查 WebGL 是否可用
if (!gl) {
  alert('您的浏览器不支持 WebGL');
  throw new Error('WebGL not supported');
}

console.log('WebGL 初始化成功');
console.log('WebGL 版本:', gl.getParameter(gl.VERSION));
console.log('GLSL 版本:', gl.getParameter(gl.SHADING_LANGUAGE_VERSION));

3. 编写着色器

WebGL 使用 GLSL(OpenGL Shading Language)编写着色器:

顶点着色器(vertex shader)

glsl
// 顶点着色器源码
const vertexShaderSource = `
  attribute vec4 a_Position;
  
  void main() {
    gl_Position = a_Position;
    gl_PointSize = 10.0;
  }
`;

片段着色器(fragment shader)

glsl
// 片段着色器源码
const fragmentShaderSource = `
  precision mediump float;
  
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
  }
`;

4. 创建着色器程序

将着色器源码添加到 main.js

javascript
// 创建着色器
function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  
  // 检查编译状态
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error('着色器编译失败:', gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
  
  return shader;
}

// 创建着色器程序
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)) {
    console.error('着色器程序链接失败:', gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
    return null;
  }
  
  return program;
}

// 创建顶点着色器
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
// 创建片段着色器
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
// 创建程序
const program = createProgram(gl, vertexShader, fragmentShader);

// 使用着色器程序
gl.useProgram(program);

5. 设置顶点数据

javascript
// 顶点数据
const vertices = new Float32Array([
  0.0,  0.5,   // 顶点 1
 -0.5, -0.5,   // 顶点 2
  0.5, -0.5    // 顶点 3
]);

// 创建缓冲区
const vertexBuffer = gl.createBuffer();

// 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

// 写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 获取 attribute 变量位置
const a_Position = gl.getAttribLocation(program, 'a_Position');

// 将缓冲区数据分配给 attribute 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

// 启用 attribute 变量
gl.enableVertexAttribArray(a_Position);

6. 绘制三角形

javascript
// 设置清除颜色(RGBA)
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 黑色背景

// 清除颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);

7. 完整代码

完整的 main.js 文件:

javascript
// ========== 着色器源码 ==========
const vertexShaderSource = `
  attribute vec4 a_Position;
  
  void main() {
    gl_Position = a_Position;
  }
`;

const fragmentShaderSource = `
  precision mediump float;
  
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
`;

// ========== 工具函数 ==========
function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error('着色器编译失败:', gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
  
  return shader;
}

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)) {
    console.error('程序链接失败:', gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
    return null;
  }
  
  return program;
}

// ========== 主程序 ==========
function main() {
  // 获取 Canvas 和 WebGL 上下文
  const canvas = document.getElementById('glCanvas');
  const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
  
  if (!gl) {
    alert('您的浏览器不支持 WebGL');
    return;
  }
  
  // 创建着色器和程序
  const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
  const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
  const program = createProgram(gl, vertexShader, fragmentShader);
  
  // 使用程序
  gl.useProgram(program);
  
  // 设置顶点数据
  const vertices = new Float32Array([
    0.0,  0.5,
   -0.5, -0.5,
    0.5, -0.5
  ]);
  
  // 创建并绑定缓冲区
  const vertexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
  
  // 配置 attribute
  const a_Position = gl.getAttribLocation(program, 'a_Position');
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(a_Position);
  
  // 清除画布并绘制
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
}

// 页面加载完成后执行
window.onload = main;

Canvas 尺寸处理

像素尺寸 vs CSS 尺寸

Canvas 有两种尺寸概念,理解它们非常重要:

尺寸类型设置方式作用
像素尺寸canvas.width/height实际渲染分辨率
CSS 尺寸style.width/height显示尺寸

高 DPI 显示支持

在高分辨率屏幕(如 Retina)上,需要特殊处理以保证清晰度:

javascript
function setupCanvas(canvas) {
  // 获取设备像素比
  const dpr = window.devicePixelRatio || 1;
  
  // 获取 CSS 显示尺寸
  const rect = canvas.getBoundingClientRect();
  
  // 设置像素尺寸(考虑设备像素比)
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  
  // 设置 CSS 尺寸
  canvas.style.width = rect.width + 'px';
  canvas.style.height = rect.height + 'px';
  
  return dpr;
}

// 使用示例
const canvas = document.getElementById('glCanvas');
const dpr = setupCanvas(canvas);
const gl = canvas.getContext('webgl');

// 重要:更新视口
gl.viewport(0, 0, canvas.width, canvas.height);

响应式 Canvas

处理窗口大小变化:

javascript
function resizeCanvas() {
  const canvas = document.getElementById('glCanvas');
  const dpr = window.devicePixelRatio || 1;
  
  // 获取容器尺寸
  const container = canvas.parentElement;
  const width = container.clientWidth;
  const height = container.clientHeight;
  
  // 设置尺寸
  canvas.width = width * dpr;
  canvas.height = height * dpr;
  canvas.style.width = width + 'px';
  canvas.style.height = height + 'px';
  
  // 更新视口
  const gl = canvas.getContext('webgl');
  gl.viewport(0, 0, canvas.width, canvas.height);
  
  // 重新渲染
  render();
}

// 监听窗口大小变化
window.addEventListener('resize', resizeCanvas);

// 初始化时调用
resizeCanvas();

完整的 Canvas 初始化函数

javascript
/**
 * 初始化 WebGL Canvas
 * @param {string|HTMLCanvasElement} canvas - Canvas 元素或 ID
 * @param {Object} options - 配置选项
 * @returns {Object} - 包含 gl 和 canvas 的对象
 */
function initWebGL(canvas, options = {}) {
  // 获取 Canvas 元素
  if (typeof canvas === 'string') {
    canvas = document.getElementById(canvas);
  }
  
  // 默认选项
  const defaults = {
    alpha: false,           // 是否支持透明
    antialias: true,        // 是否启用抗锯齿
    depth: true,            // 是否启用深度缓冲
    stencil: false,         // 是否启用模板缓冲
    preserveDrawingBuffer: false,  // 是否保留绘制缓冲
    premultipliedAlpha: true       // 是否预乘 Alpha
  };
  
  const settings = { ...defaults, ...options };
  
  // 处理高 DPI
  const dpr = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();
  
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  canvas.style.width = rect.width + 'px';
  canvas.style.height = rect.height + 'px';
  
  // 获取 WebGL 上下文
  const gl = canvas.getContext('webgl2', settings) || 
             canvas.getContext('webgl', settings) ||
             canvas.getContext('experimental-webgl', settings);
  
  if (!gl) {
    throw new Error('WebGL 不可用');
  }
  
  // 设置视口
  gl.viewport(0, 0, canvas.width, canvas.height);
  
  // 输出调试信息
  console.log('WebGL 版本:', gl.getParameter(gl.VERSION));
  console.log('GLSL 版本:', gl.getParameter(gl.SHADING_LANGUAGE_VERSION));
  console.log('渲染器:', gl.getParameter(gl.RENDERER));
  console.log('设备像素比:', dpr);
  
  return { gl, canvas, dpr };
}

// 使用示例
const { gl, canvas, dpr } = initWebGL('glCanvas', {
  alpha: false,
  antialias: true
});

WebGL 渲染流程

理解 WebGL 的渲染流程至关重要:

code
┌──────────────┐
│  顶点数据     │
└──────┬───────┘
       ↓
┌──────────────┐
│ 顶点着色器    │  ← 处理每个顶点
└──────┬───────┘
       ↓
┌──────────────┐
│ 图元装配      │  ← 组装点、线、三角形
└──────┬───────┘
       ↓
┌──────────────┐
│ 光栅化        │  ← 将图形转换为片段
└──────┬───────┘
       ↓
┌──────────────┐
│ 片段着色器    │  ← 计算每个像素颜色
└──────┬───────┘
       ↓
┌──────────────┐
│ 帧缓冲        │  ← 输出到屏幕
└──────────────┘

流程详解

  1. 顶点数据:定义几何图形的顶点坐标、颜色等信息
  2. 顶点着色器:对每个顶点进行变换和属性计算
  3. 图元装配:将顶点组装成点、线、三角形等基本图元
  4. 光栅化:将几何图形转换为屏幕上的像素片段
  5. 片段着色器:计算每个片段的最终颜色
  6. 帧缓冲:存储最终渲染结果并显示

常见问题排查

1. Canvas 黑屏

原因

  • 着色器编译失败
  • 程序链接失败
  • 未清除颜色缓冲区
  • 顶点数据错误

解决方法

javascript
// 检查着色器编译错误
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
  console.error('编译错误:', gl.getShaderInfoLog(shader));
}

// 检查程序链接错误
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  console.error('链接错误:', gl.getProgramInfoLog(program));
}

// 确保清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

2. 着色器变量未找到

原因

  • 变量名拼写错误
  • 变量未使用被优化掉

解决方法

javascript
const a_Position = gl.getAttribLocation(program, 'a_Position');

if (a_Position < 0) {
  console.error('未找到 attribute 变量 a_Position');
}

3. 坐标系问题

WebGL 使用归一化设备坐标(NDC),范围是 [-1, 1]:

javascript
// 坐标系说明
// (0.0, 1.0)  ← 左上
//      |
//      |-------- (1.0, 0.0) 右下
//      |
// (-1.0, -1.0) ← 左下角

// Canvas 坐标转 WebGL 坐标
function canvasToWebGL(x, y, canvasWidth, canvasHeight) {
  const glX = (x / canvasWidth) * 2 - 1;
  const glY = -((y / canvasHeight) * 2 - 1);
  return [glX, glY];
}

4. WebGL 错误检查

WebGL 不会自动抛出错误,需要手动检查:

javascript
// 获取错误信息
function checkError(gl, location) {
  const error = gl.getError();
  if (error !== gl.NO_ERROR) {
    const errors = {
      [gl.INVALID_ENUM]: 'INVALID_ENUM',
      [gl.INVALID_VALUE]: 'INVALID_VALUE',
      [gl.INVALID_OPERATION]: 'INVALID_OPERATION',
      [gl.INVALID_FRAMEBUFFER_OPERATION]: 'INVALID_FRAMEBUFFER_OPERATION',
      [gl.OUT_OF_MEMORY]: 'OUT_OF_MEMORY',
      [gl.CONTEXT_LOST_WEBGL]: 'CONTEXT_LOST_WEBGL'
    };
    console.error(`WebGL 错误 ${location}:`, errors[error] || error);
  }
}

// 使用示例
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
checkError(gl, 'bindBuffer');

调试技巧

1. 使用浏览器开发工具

Chrome DevTools

  • 打开开发者工具(F12)
  • Console 标签查看错误信息
  • Sources 标签设置断点调试

WebGL 调试

javascript
// 查询 WebGL 信息
console.log('WebGL 版本:', gl.getParameter(gl.VERSION));
console.log('GLSL 版本:', gl.getParameter(gl.SHADING_LANGUAGE_VERSION));
console.log('渲染器:', gl.getParameter(gl.RENDERER));
console.log('厂商:', gl.getParameter(gl.VENDOR));

2. 安装调试扩展

Spector.js

  1. 安装 Chrome 扩展
  2. 点击扩展图标开始捕获
  3. 查看每一帧的 WebGL 调用序列

WebGL Inspector

  1. 安装扩展
  2. 刷新页面
  3. 点击 "Inspect" 查看详细状态

3. 添加调试输出

javascript
// 在关键位置添加日志
console.log('顶点数量:', vertexCount);
console.log('缓冲区大小:', gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE));
console.log('当前程序:', gl.getParameter(gl.CURRENT_PROGRAM));

4. 着色器调试

在着色器中输出调试信息:

glsl
// 在片段着色器中输出变量值
void main() {
  // 输出位置作为颜色
  gl_FragColor = vec4(v_Position * 0.5 + 0.5, 1.0);
  
  // 输出法线
  // gl_FragColor = vec4(v_Normal * 0.5 + 0.5, 1.0);
  
  // 输出纹理坐标
  // gl_FragColor = vec4(v_TexCoord, 0.0, 1.0);
}

性能基础

1. 减少 WebGL 状态切换

javascript
// ❌ 不好的做法:频繁切换
for (let i = 0; i < 100; i++) {
  gl.useProgram(programs[i]);
  gl.bindTexture(gl.TEXTURE_2D, textures[i]);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
}

// ✅ 好的做法:批量处理
gl.useProgram(program);
for (let i = 0; i < 100; i++) {
  gl.bindTexture(gl.TEXTURE_2D, textures[i]);
  gl.drawArrays(gl.TRIANGLES, i * 3, 3);
}

2. 使用缓冲区批量绘制

javascript
// 批量上传顶点数据
const vertices = new Float32Array([
  // 第一个三角形
  0.0, 0.5,
 -0.5, -0.5,
  0.5, -0.5,
  // 第二个三角形
  0.5, 0.5,
  0.0, -0.5,
  1.0, -0.5
]);

gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, 6); // 一次绘制两个三角形

动画循环

requestAnimationFrame

使用 requestAnimationFrame 实现流畅动画:

javascript
function render(time) {
  // 清除缓冲区
  gl.clear(gl.COLOR_BUFFER_BIT);
  
  // 更新场景
  updateScene(time);
  
  // 绘制
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  
  // 请求下一帧
  requestAnimationFrame(render);
}

// 开始动画
requestAnimationFrame(render);

时间控制

javascript
let lastTime = 0;

function render(currentTime) {
  // 计算时间差
  const deltaTime = currentTime - lastTime;
  lastTime = currentTime;
  
  // 转换为秒
  const seconds = currentTime / 1000;
  
  // 使用时间更新动画
  updateAnimation(deltaTime, seconds);
  
  // 绘制
  draw();
  
  requestAnimationFrame(render);
}

requestAnimationFrame(render);

下一步学习

恭喜你完成了第一个 WebGL 程序!接下来建议:

  1. 核心概念:深入理解 WebGL 渲染管线
  2. 着色器语言:学习 GLSL 编程
  3. 几何图形绘制:绘制更复杂的图形

参考资源