乐闻世界logo
搜索文章和话题

WebGL 性能优化有哪些常用技巧?

3月6日 21:57

WebGL 性能优化概述

WebGL 性能优化是 3D Web 应用开发的关键。由于 JavaScript 和 GPU 之间的通信开销,以及移动设备的资源限制,合理的优化策略能显著提升渲染性能。

1. 减少绘制调用(Draw Calls)

问题

每次 gl.drawArraysgl.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. 遮挡剔除和视锥剔除

视锥剔除

javascript
function 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)

javascript
const 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
标签:WebGL