Skip to content

第11章:性能优化与调试

11.1 章节概述

WebGL 应用的性能直接影响用户体验。本章将深入讲解如何优化 WebGL 应用的性能,以及如何调试常见问题。

本章将深入讲解:

  • 性能瓶颈分析:CPU vs GPU
  • Draw Call 优化:批处理、实例化
  • 着色器优化:避免分支、精度选择
  • 内存管理:纹理、缓冲区
  • 调试工具:Spector.js、Chrome DevTools

11.2 性能分析基础

11.2.1 渲染管线瓶颈

性能瓶颈识别

WebGL 渲染涉及 CPU 和 GPU 两端

CPU 端瓶颈:
──────────────────────────────────
- 过多的 Draw Call
- JavaScript 计算量大
- 频繁的状态切换
- 数据准备和上传

表现:GPU 空闲等待,帧率受 CPU 限制


GPU 端瓶颈:
──────────────────────────────────
- 顶点处理:复杂的顶点着色器、大量顶点
- 光栅化:大量小三角形
- 片段处理:复杂着色器、高分辨率、过度绘制
- 内存带宽:大纹理、频繁纹理切换

表现:CPU 等待 GPU 完成


判断方法:
──────────────────────────────────
1. 减少渲染分辨率
   - 帧率显著提升 → GPU 片段处理瓶颈
   - 帧率变化不大 → 可能是 CPU 或顶点瓶颈

2. 减少 Draw Call
   - 帧率提升 → CPU 瓶颈
   
3. 简化着色器
   - 帧率提升 → 着色器瓶颈

11.2.2 性能测量

javascript
/**
 * 性能监控类
 */
class PerformanceMonitor {
    constructor() {
        this.frames = 0;
        this.lastTime = performance.now();
        this.fps = 0;
        this.frameTime = 0;
        this.frameTimes = [];
        this.maxSamples = 60;
    }
    
    begin() {
        this.frameStart = performance.now();
    }
    
    end() {
        const now = performance.now();
        const frameTime = now - this.frameStart;
        
        this.frameTimes.push(frameTime);
        if (this.frameTimes.length > this.maxSamples) {
            this.frameTimes.shift();
        }
        
        this.frames++;
        
        // 每秒更新 FPS
        if (now - this.lastTime >= 1000) {
            this.fps = this.frames;
            this.frameTime = this.frameTimes.reduce((a, b) => a + b) / this.frameTimes.length;
            this.frames = 0;
            this.lastTime = now;
        }
    }
    
    getStats() {
        return {
            fps: this.fps,
            frameTime: this.frameTime.toFixed(2),
            minFrameTime: Math.min(...this.frameTimes).toFixed(2),
            maxFrameTime: Math.max(...this.frameTimes).toFixed(2)
        };
    }
}

// 使用
const perfMonitor = new PerformanceMonitor();

function render() {
    perfMonitor.begin();
    
    // 渲染代码...
    
    perfMonitor.end();
    
    requestAnimationFrame(render);
}

// 定期输出性能数据
setInterval(() => {
    console.log(perfMonitor.getStats());
}, 1000);

11.2.3 GPU 时间测量(WebGL 2.0 扩展)

javascript
// 使用 EXT_disjoint_timer_query_webgl2 扩展
const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');

if (ext) {
    const query = gl.createQuery();
    
    gl.beginQuery(ext.TIME_ELAPSED_EXT, query);
    // 绘制命令...
    gl.endQuery(ext.TIME_ELAPSED_EXT);
    
    // 稍后获取结果(异步)
    function checkQuery() {
        if (gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) {
            const timeElapsed = gl.getQueryParameter(query, gl.QUERY_RESULT);
            console.log('GPU 时间:', timeElapsed / 1000000, 'ms');
        } else {
            requestAnimationFrame(checkQuery);
        }
    }
    
    requestAnimationFrame(checkQuery);
}

11.3 Draw Call 优化

11.3.1 减少 Draw Call

Draw Call 开销

每次 Draw Call 都有 CPU 开销:
- 驱动程序验证状态
- 命令缓冲区提交
- CPU-GPU 同步

优化策略:
──────────────────────────────────

1. 批处理(Batching)
   将多个小物体合并成一个大的 Draw Call
   
   Before:          After:
   draw(mesh1)  →   draw(combinedMesh)
   draw(mesh2)
   draw(mesh3)


2. 实例化(Instancing)
   相同几何体,不同变换
   
   Before:          After:
   for 1000 次:  →  drawInstanced(mesh, 1000)
     draw(mesh)


3. 纹理图集(Texture Atlas)
   多个纹理合并成一张
   避免纹理切换


4. 排序绘制顺序
   按材质、纹理排序
   减少状态切换

11.3.2 动态批处理实现

javascript
/**
 * 动态批处理器
 */
class DynamicBatcher {
    constructor(gl, maxVertices = 65536) {
        this.gl = gl;
        this.maxVertices = maxVertices;
        
        // 预分配缓冲区
        this.positions = new Float32Array(maxVertices * 3);
        this.normals = new Float32Array(maxVertices * 3);
        this.uvs = new Float32Array(maxVertices * 2);
        this.colors = new Float32Array(maxVertices * 4);
        
        this.vertexCount = 0;
        this.currentTexture = null;
        
        this.positionBuffer = gl.createBuffer();
        this.normalBuffer = gl.createBuffer();
        this.uvBuffer = gl.createBuffer();
        this.colorBuffer = gl.createBuffer();
    }
    
    begin() {
        this.vertexCount = 0;
    }
    
    addMesh(mesh, transform, color = [1, 1, 1, 1]) {
        // 检查是否需要 flush
        if (this.vertexCount + mesh.vertexCount > this.maxVertices) {
            this.flush();
        }
        
        // 检查纹理是否改变
        if (mesh.texture !== this.currentTexture) {
            this.flush();
            this.currentTexture = mesh.texture;
        }
        
        // 添加顶点数据(应用变换)
        for (let i = 0; i < mesh.vertexCount; i++) {
            const vi = this.vertexCount + i;
            const mi = i * 3;
            
            // 变换位置
            const pos = this.transformPoint(
                mesh.positions[mi],
                mesh.positions[mi + 1],
                mesh.positions[mi + 2],
                transform
            );
            
            this.positions[vi * 3] = pos[0];
            this.positions[vi * 3 + 1] = pos[1];
            this.positions[vi * 3 + 2] = pos[2];
            
            // 复制其他属性...
        }
        
        this.vertexCount += mesh.vertexCount;
    }
    
    flush() {
        if (this.vertexCount === 0) return;
        
        const gl = this.gl;
        
        // 上传数据
        gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, this.positions.subarray(0, this.vertexCount * 3), gl.DYNAMIC_DRAW);
        
        // ... 其他缓冲区
        
        // 绑定纹理
        if (this.currentTexture) {
            gl.bindTexture(gl.TEXTURE_2D, this.currentTexture);
        }
        
        // 绘制
        gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
        
        this.vertexCount = 0;
    }
    
    end() {
        this.flush();
    }
}

11.3.3 按状态排序

javascript
/**
 * 渲染队列排序
 */
class RenderQueue {
    constructor() {
        this.opaqueQueue = [];
        this.transparentQueue = [];
    }
    
    add(renderable) {
        if (renderable.isTransparent) {
            this.transparentQueue.push(renderable);
        } else {
            this.opaqueQueue.push(renderable);
        }
    }
    
    sort(cameraPosition) {
        // 不透明物体:按材质/纹理排序(减少状态切换)
        this.opaqueQueue.sort((a, b) => {
            // 先按着色器排序
            if (a.shader !== b.shader) {
                return a.shader.id - b.shader.id;
            }
            // 再按纹理排序
            if (a.texture !== b.texture) {
                return a.texture.id - b.texture.id;
            }
            // 最后按距离排序(近到远,利用深度测试提前丢弃)
            return a.distanceTo(cameraPosition) - b.distanceTo(cameraPosition);
        });
        
        // 透明物体:从远到近排序
        this.transparentQueue.sort((a, b) => {
            return b.distanceTo(cameraPosition) - a.distanceTo(cameraPosition);
        });
    }
    
    render() {
        let currentShader = null;
        let currentTexture = null;
        
        // 渲染不透明物体
        for (const obj of this.opaqueQueue) {
            // 只在必要时切换状态
            if (obj.shader !== currentShader) {
                obj.shader.use();
                currentShader = obj.shader;
            }
            if (obj.texture !== currentTexture) {
                obj.texture.bind();
                currentTexture = obj.texture;
            }
            obj.draw();
        }
        
        // 渲染透明物体
        // ...
    }
    
    clear() {
        this.opaqueQueue.length = 0;
        this.transparentQueue.length = 0;
    }
}

11.4 着色器优化

11.4.1 避免分支

glsl
// ❌ 不好:动态分支
if (u_lightType == 0) {
    // 点光源计算
} else if (u_lightType == 1) {
    // 方向光计算
} else {
    // 聚光灯计算
}

// ✅ 好:使用不同的着色器程序
// 为每种光源类型编译专门的着色器

// ✅ 好:使用混合代替分支
float isPoint = float(u_lightType == 0);
float isDir = float(u_lightType == 1);

result = pointLightCalc * isPoint + 
         dirLightCalc * isDir + 
         spotLightCalc * (1.0 - isPoint - isDir);

// ✅ 好:使用 step/smoothstep
// 代替 if (x > threshold)
float factor = step(threshold, x);

11.4.2 精度选择

glsl
// 根据需要选择精度
precision highp float;   // 位置、变换矩阵
precision mediump float; // 颜色、UV
precision lowp float;    // 颜色查找

// 在片段着色器中
// ❌ 全部使用 highp
precision highp float;

// ✅ 按需使用
precision mediump float;  // 默认中精度

highp vec4 worldPos;      // 位置需要高精度
lowp vec4 color;          // 颜色可以低精度

11.4.3 减少计算

glsl
// ❌ 在循环中重复计算
for (int i = 0; i < NUM_LIGHTS; i++) {
    vec3 L = normalize(u_lights[i].position - v_worldPos);  // 每次都归一化
    float NdotL = dot(N, L);
    // ...
}

// ✅ 预计算不变的值
vec3 N = normalize(v_normal);  // 只归一化一次

for (int i = 0; i < NUM_LIGHTS; i++) {
    // ...
}


// ❌ 使用 pow 计算整数幂
float x2 = pow(x, 2.0);
float x3 = pow(x, 3.0);

// ✅ 直接乘法
float x2 = x * x;
float x3 = x * x * x;


// ❌ 计算不需要的值
vec4 result;
result.rgb = texture(u_tex, uv).rgb;
result.a = 1.0;  // alpha 固定为 1
// 但纹理采样读取了 4 个通道

// ✅ 使用 swizzle 或专用纹理
vec3 rgb = texture(u_tex, uv).rgb;

11.4.4 向量化操作

glsl
// ❌ 标量操作
float r = color.r * 0.5;
float g = color.g * 0.5;
float b = color.b * 0.5;

// ✅ 向量操作
vec3 result = color.rgb * 0.5;


// ❌ 分别计算点积分量
float d = a.x * b.x + a.y * b.y + a.z * b.z;

// ✅ 使用内置函数
float d = dot(a, b);

11.5 纹理优化

11.5.1 纹理压缩

javascript
// 检查压缩纹理支持
const s3tc = gl.getExtension('WEBGL_compressed_texture_s3tc');
const etc = gl.getExtension('WEBGL_compressed_texture_etc');
const astc = gl.getExtension('WEBGL_compressed_texture_astc');

// 使用压缩纹理
if (s3tc) {
    gl.compressedTexImage2D(
        gl.TEXTURE_2D,
        0,
        s3tc.COMPRESSED_RGBA_S3TC_DXT5_EXT,
        width, height,
        0,
        compressedData
    );
}

// 压缩格式对比
// DXT (S3TC): PC 常用,不支持移动端
// ETC: Android 支持
// PVRTC: iOS 支持
// ASTC: 高质量,新设备支持

// 使用 Basis Universal 或 KTX2 格式
// 可以转码到目标平台的格式

11.5.2 Mipmap 策略

javascript
// 正确使用 Mipmap
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.generateMipmap(gl.TEXTURE_2D);

// 限制最大 mipmap 级别
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, 4);

// 各向异性过滤(提高斜视质量)
const aniso = gl.getExtension('EXT_texture_filter_anisotropic');
if (aniso) {
    const max = gl.getParameter(aniso.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
    gl.texParameterf(gl.TEXTURE_2D, aniso.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(8, max));
}

11.5.3 纹理图集

javascript
/**
 * 纹理图集管理器
 */
class TextureAtlas {
    constructor(gl, width, height) {
        this.gl = gl;
        this.width = width;
        this.height = height;
        
        this.texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, this.texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        
        this.regions = new Map();
        this.packer = new RectanglePacker(width, height);
    }
    
    add(name, image) {
        const region = this.packer.pack(image.width, image.height);
        if (!region) {
            console.error('图集空间不足');
            return null;
        }
        
        const gl = this.gl;
        gl.bindTexture(gl.TEXTURE_2D, this.texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D, 0,
            region.x, region.y,
            gl.RGBA, gl.UNSIGNED_BYTE,
            image
        );
        
        // 存储 UV 坐标
        const uvRegion = {
            u1: region.x / this.width,
            v1: region.y / this.height,
            u2: (region.x + image.width) / this.width,
            v2: (region.y + image.height) / this.height
        };
        
        this.regions.set(name, uvRegion);
        return uvRegion;
    }
    
    getUV(name) {
        return this.regions.get(name);
    }
}

11.6 内存管理

11.6.1 资源清理

javascript
/**
 * 资源管理器
 */
class ResourceManager {
    constructor(gl) {
        this.gl = gl;
        this.textures = new Map();
        this.buffers = new Map();
        this.programs = new Map();
        this.framebuffers = new Map();
    }
    
    createTexture(name, image) {
        const gl = this.gl;
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        gl.generateMipmap(gl.TEXTURE_2D);
        
        this.textures.set(name, texture);
        return texture;
    }
    
    deleteTexture(name) {
        const texture = this.textures.get(name);
        if (texture) {
            this.gl.deleteTexture(texture);
            this.textures.delete(name);
        }
    }
    
    // 清理所有资源
    dispose() {
        const gl = this.gl;
        
        for (const texture of this.textures.values()) {
            gl.deleteTexture(texture);
        }
        
        for (const buffer of this.buffers.values()) {
            gl.deleteBuffer(buffer);
        }
        
        for (const program of this.programs.values()) {
            gl.deleteProgram(program);
        }
        
        for (const fb of this.framebuffers.values()) {
            gl.deleteFramebuffer(fb);
        }
        
        this.textures.clear();
        this.buffers.clear();
        this.programs.clear();
        this.framebuffers.clear();
    }
}

11.6.2 对象池

javascript
/**
 * 对象池 - 避免频繁创建和销毁
 */
class ObjectPool {
    constructor(createFn, resetFn, initialSize = 100) {
        this.createFn = createFn;
        this.resetFn = resetFn;
        this.pool = [];
        
        // 预创建
        for (let i = 0; i < initialSize; i++) {
            this.pool.push(createFn());
        }
    }
    
    acquire() {
        if (this.pool.length > 0) {
            return this.pool.pop();
        }
        return this.createFn();
    }
    
    release(obj) {
        this.resetFn(obj);
        this.pool.push(obj);
    }
}

// 使用示例:粒子池
const particlePool = new ObjectPool(
    () => ({ x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, life: 0 }),
    (p) => { p.x = p.y = p.z = p.vx = p.vy = p.vz = p.life = 0; }
);

// 获取粒子
const particle = particlePool.acquire();

// 归还粒子
particlePool.release(particle);

11.7 常见性能问题

11.7.1 问题清单

常见性能问题及解决方案

1. 过多的 Draw Call
──────────────────────────────────
症状:CPU 使用率高,GPU 空闲
解决:批处理、实例化、合并网格


2. 着色器编译卡顿
──────────────────────────────────
症状:首次渲染时卡顿
解决:预编译着色器、使用着色器缓存


3. 纹理上传阻塞
──────────────────────────────────
症状:加载新纹理时卡顿
解决:异步加载、渐进式加载、使用 ImageBitmap


4. 过度绘制(Overdraw)
──────────────────────────────────
症状:GPU 片段处理瓶颈
解决:从前到后渲染、遮挡剔除、LOD


5. 内存泄漏
──────────────────────────────────
症状:内存持续增长
解决:正确删除资源、使用资源管理器


6. GC 停顿
──────────────────────────────────
症状:周期性卡顿
解决:对象池、避免临时对象

11.7.2 异步纹理加载

javascript
/**
 * 异步纹理加载
 */
async function loadTextureAsync(gl, url) {
    // 使用 ImageBitmap 进行离线解码
    const response = await fetch(url);
    const blob = await response.blob();
    const imageBitmap = await createImageBitmap(blob);
    
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageBitmap);
    gl.generateMipmap(gl.TEXTURE_2D);
    
    imageBitmap.close();  // 释放 ImageBitmap
    
    return texture;
}

// 批量加载
async function loadTextures(gl, urls) {
    const promises = urls.map(url => loadTextureAsync(gl, url));
    return Promise.all(promises);
}

11.8 调试工具

11.8.1 Spector.js

javascript
// 使用 Spector.js 捕获帧
// 1. 安装 Chrome 扩展或通过 npm
// npm install spectorjs

import { Spector } from 'spectorjs';

const spector = new Spector();

// 捕获下一帧
spector.captureNextFrame(canvas);

// 或手动捕获
spector.startCapture(canvas, 100);  // 捕获 100 个命令
// ... 渲染
spector.stopCapture();

// 查看捕获结果
spector.onCapture.add((capture) => {
    console.log('捕获的命令:', capture.commands);
    console.log('状态:', capture.contexts);
});

11.8.2 Chrome DevTools

javascript
// 使用 console 分组输出
console.group('Frame');
console.log('Draw calls:', drawCallCount);
console.log('Triangles:', triangleCount);
console.groupEnd();

// 性能标记
performance.mark('render-start');
// 渲染代码
performance.mark('render-end');
performance.measure('render', 'render-start', 'render-end');

// 获取测量结果
const measures = performance.getEntriesByName('render');
console.log('渲染时间:', measures[measures.length - 1].duration, 'ms');

11.8.3 WebGL 错误检查

javascript
/**
 * WebGL 错误检查
 */
function checkGLError(gl, operation = '') {
    const error = gl.getError();
    if (error !== gl.NO_ERROR) {
        const errorNames = {
            [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 错误 ${operation}: ${errorNames[error] || error}`);
        return false;
    }
    return true;
}

// 开发模式下检查每个操作
function debugGL(gl) {
    const originalFunctions = {};
    
    for (const key of Object.keys(gl.__proto__)) {
        if (typeof gl[key] === 'function') {
            originalFunctions[key] = gl[key];
            
            gl[key] = function(...args) {
                const result = originalFunctions[key].apply(gl, args);
                checkGLError(gl, key);
                return result;
            };
        }
    }
}

11.8.4 着色器调试

glsl
// 可视化调试

// 可视化法线
gl_FragColor = vec4(v_normal * 0.5 + 0.5, 1.0);

// 可视化 UV
gl_FragColor = vec4(v_texCoord, 0.0, 1.0);

// 可视化深度
float depth = gl_FragCoord.z;
gl_FragColor = vec4(vec3(depth), 1.0);

// 可视化世界位置
gl_FragColor = vec4(fract(v_worldPos), 1.0);

// 热力图可视化(用于显示计算量)
// 根据某个值映射到颜色
vec3 heatmap(float t) {
    return vec3(
        clamp(1.5 - abs(4.0 * t - 3.0), 0.0, 1.0),
        clamp(1.5 - abs(4.0 * t - 2.0), 0.0, 1.0),
        clamp(1.5 - abs(4.0 * t - 1.0), 0.0, 1.0)
    );
}

11.9 本章小结

优化策略总结

领域优化方法
Draw Call批处理、实例化、排序
着色器避免分支、适当精度、向量化
纹理压缩、Mipmap、图集
内存资源管理、对象池、及时清理
加载异步加载、渐进式

调试工具

工具用途
Spector.js捕获和分析 WebGL 调用
Chrome DevTools性能分析、内存检查
RenderDocGPU 调试(需要特殊构建)

11.10 练习题

基础练习

  1. 实现性能监控,显示 FPS 和帧时间

  2. 使用实例化渲染优化大量相同物体的绘制

进阶练习

  1. 实现简单的批处理系统

  2. 比较不同精度和优化级别的着色器性能

挑战练习

  1. 使用 Spector.js 分析和优化一个复杂场景

下一章预告:在第12章中,我们将通过完整的实战案例,综合运用所学知识。


文档版本:v1.0
字数统计:约 11,000 字
代码示例:45+ 个

如有转载或 CV 的请标注本站原文地址