WebGL 快速入门
环境准备
浏览器要求
WebGL 需要支持 WebGL API 的现代浏览器:
| 浏览器 | WebGL 1.0 | WebGL 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. 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:
- 安装 Chrome 扩展
- 点击扩展图标开始捕获
- 查看每一帧的 WebGL 调用序列
WebGL Inspector:
- 安装扩展
- 刷新页面
- 点击 "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 程序!接下来建议: