WebGL 性能优化概述
WebGL 性能优化是 3D Web 应用开发的关键。由于 JavaScript 和 GPU 之间的通信开销,以及移动设备的资源限制,合理的优化策略能显著提升渲染性能。
1. 减少绘制调用(Draw Calls)
问题
每次 gl.drawArrays 或 gl.drawElements 都有 CPU 到 GPU 的通信开销。
优化方案
批量绘制(Batching)
javascript// 优化前:多次绘制调用 for (let mesh of meshes) { gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vbo); gl.drawArrays(gl.TRIANGLES, 0, mesh.vertexCount); } // 优化后:合并到一个缓冲区 const mergedBuffer = mergeMeshes(meshes); gl.bindBuffer(gl.ARRAY_BUFFER, mergedBuffer); gl.drawArrays(gl.TRIANGLES, 0, totalVertexCount);
实例化渲染(Instanced Rendering)
javascript// WebGL 2.0 原生支持 // 一次绘制调用渲染多个相同几何体 const instanceCount = 1000; gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);
2. 减少状态切换
问题
频繁切换着色器程序、纹理、缓冲区等状态会造成性能开销。
优化方案
按状态排序
javascript// 按着色器程序排序 meshes.sort((a, b) => a.program.id - b.program.id); // 按纹理排序 meshes.sort((a, b) => a.texture.id - b.texture.id); let currentProgram = null; let currentTexture = null; for (let mesh of meshes) { // 只在需要时切换程序 if (mesh.program !== currentProgram) { gl.useProgram(mesh.program); currentProgram = mesh.program; } // 只在需要时切换纹理 if (mesh.texture !== currentTexture) { gl.bindTexture(gl.TEXTURE_2D, mesh.texture); currentTexture = mesh.texture; } mesh.draw(); }
使用纹理图集(Texture Atlas)
javascript// 将多个小纹理合并为一个大纹理 // 减少纹理绑定切换次数 const atlasTexture = createTextureAtlas([ 'texture1.png', 'texture2.png', 'texture3.png' ]); // 在着色器中使用纹理坐标偏移 uniform vec2 u_atlasOffset; uniform vec2 u_atlasScale; vec2 atlasCoord = a_texCoord * u_atlasScale + u_atlasOffset; vec4 color = texture2D(u_texture, atlasCoord);
3. 优化着色器
顶点着色器优化
glsl// 优化前:在顶点着色器中进行复杂计算 attribute vec3 a_position; uniform mat4 u_modelMatrix; uniform mat4 u_viewMatrix; uniform mat4 u_projectionMatrix; void main() { // 每次顶点都进行矩阵乘法 mat4 mvp = u_projectionMatrix * u_viewMatrix * u_modelMatrix; gl_Position = mvp * vec4(a_position, 1.0); } // 优化后:在 CPU 预计算 MVP 矩阵 uniform mat4 u_mvpMatrix; void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); }
片段着色器优化
glsl// 优化前:复杂的逐像素计算 void main() { vec3 lightDir = normalize(u_lightPos - v_worldPos); float diff = max(dot(v_normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // ... 更多计算 } // 优化后:顶点着色器计算光照 // 顶点着色器 varying vec3 v_lightIntensity; void main() { // 在顶点级别计算光照 vec3 lightDir = normalize(u_lightPos - worldPos); float diff = max(dot(normal, lightDir), 0.0); v_lightIntensity = diff * u_lightColor; } // 片段着色器 void main() { // 使用插值后的光照强度 gl_FragColor = vec4(v_lightIntensity * textureColor, 1.0); }
使用适当精度
glsl// 高精度(highp)- 顶点位置、变换矩阵 attribute highp vec3 a_position; uniform highp mat4 u_mvpMatrix; // 中精度(mediump)- 颜色、纹理坐标 attribute mediump vec2 a_texCoord; varying mediump vec2 v_texCoord; // 低精度(lowp)- 光照计算结果 varying lowp vec3 v_lightColor;
4. 缓冲区优化
使用 VAO 减少状态设置
javascript// WebGL 2.0 或扩展支持 const vao = gl.createVertexArray(); gl.bindVertexArray(vao); // 配置顶点属性(只执行一次) gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(1); gl.bindVertexArray(null); // 绘制时只需绑定 VAO gl.bindVertexArray(vao); gl.drawArrays(gl.TRIANGLES, 0, count);
使用索引绘制
javascript// 优化前:36 个顶点定义一个立方体 const vertices = new Float32Array([ // 每个面 6 个顶点,共 6 个面 // 大量重复顶点数据 ]); // 优化后:8 个顶点 + 36 个索引 const vertices = new Float32Array([ // 8 个唯一顶点 ]); const indices = new Uint16Array([ // 36 个索引定义 12 个三角形 ]); gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
使用 Interleaved Arrays
javascript// 优化前:分离的缓冲区 const positions = new Float32Array([/* ... */]); const colors = new Float32Array([/* ... */]); const texCoords = new Float32Array([/* ... */]); // 优化后:交错的顶点数据 const vertices = new Float32Array([ // x, y, z, r, g, b, u, v 0, 0, 0, 1, 0, 0, 0, 0, // 顶点 1 1, 0, 0, 0, 1, 0, 1, 0, // 顶点 2 // ... ]); // 更好的缓存局部性
5. 纹理优化
纹理压缩
javascript// 使用压缩纹理格式 const compressedExtension = gl.getExtension('WEBGL_compressed_texture_s3tc'); // 上传压缩纹理数据 gl.compressedTexImage2D( gl.TEXTURE_2D, 0, compressedExtension.COMPRESSED_RGBA_S3TC_DXT5_EXT, width, height, 0, compressedData );
Mipmap 使用
javascript// 启用 mipmap 提高渲染质量和性能 gl.generateMipmap(gl.TEXTURE_2D); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
纹理尺寸优化
- 使用 2 的幂次尺寸(支持 mipmap)
- 避免过大的纹理(内存和带宽开销)
- 根据距离使用不同分辨率的纹理(LOD)
6. 遮挡剔除和视锥剔除
视锥剔除
javascriptfunction isInFrustum(boundingBox, viewProjectionMatrix) { // 将包围盒转换到裁剪空间 // 检查是否在视锥体内 const corners = boundingBox.getCorners(); for (let corner of corners) { const clipPos = transformPoint(corner, viewProjectionMatrix); if (Math.abs(clipPos.x) <= clipPos.w && Math.abs(clipPos.y) <= clipPos.w && 0 <= clipPos.z && clipPos.z <= clipPos.w) { return true; } } return false; } // 只渲染在视锥体内的物体 for (let object of scene.objects) { if (isInFrustum(object.boundingBox, vpMatrix)) { object.render(); } }
遮挡查询(WebGL 2.0)
javascriptconst query = gl.createQuery(); // 开始遮挡查询 gl.beginQuery(gl.ANY_SAMPLES_PASSED, query); drawBoundingBox(object); gl.endQuery(gl.ANY_SAMPLES_PASSED); // 检查结果 const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE); if (available) { const visible = gl.getQueryParameter(query, gl.QUERY_RESULT) > 0; if (visible) { drawDetailedMesh(object); } }
7. 帧缓冲区优化
减少分辨率
javascript// 在高 DPI 屏幕上使用适当分辨率 const dpr = Math.min(window.devicePixelRatio, 2); canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr;
延迟渲染优化
javascript// G-Buffer 优化:使用适当精度 // 位置:RGB16F 或 RGBA16F // 法线:RGB10_A2 或 RGBA8 // 材质:RGBA8
8. JavaScript 优化
避免垃圾回收
javascript// 优化前:每帧创建新数组 function update() { const matrix = new Float32Array(16); // 创建垃圾 // ... } // 优化后:重用数组 const matrix = new Float32Array(16); function update() { // 重用 matrix,不创建新对象 mat4.identity(matrix); // ... }
使用 TypedArrays
javascript// 使用 Float32Array 而不是普通数组 const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]); gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
9. 移动端优化
减少过度绘制
javascript// 从前到后绘制不透明物体 gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); opaqueObjects.sort((a, b) => b.distance - a.distance); for (let obj of opaqueObjects) { obj.draw(); }
使用适当精度
glsl// 移动端使用 mediump 优化性能 precision mediump float;
避免复杂着色器
- 减少纹理采样次数
- 避免动态分支
- 简化光照计算
10. 性能监控
javascript// 使用 EXT_disjoint_timer_query 测量 GPU 时间 const ext = gl.getExtension('EXT_disjoint_timer_query'); const query = ext.createQueryEXT(); ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query); drawScene(); ext.endQueryEXT(ext.TIME_ELAPSED_EXT); // 获取结果 const available = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT); if (available) { const timeElapsed = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT); console.log(`GPU time: ${timeElapsed / 1000000} ms`); }
总结
| 优化方向 | 主要技巧 |
|---|---|
| 绘制调用 | 批量绘制、实例化渲染 |
| 状态切换 | 状态排序、纹理图集、VAO |
| 着色器 | 预计算、适当精度、简化计算 |
| 缓冲区 | 索引绘制、交错数组 |
| 纹理 | 压缩、mipmap、合理尺寸 |
| 剔除 | 视锥剔除、遮挡查询 |
| JavaScript | 避免 GC、TypedArrays |