Vue 3 与 WebGL 高级应用

概述

WebGL 是一种用于在浏览器中渲染高性能 2D 和 3D 图形的 API,它允许开发者直接访问 GPU 进行硬件加速渲染。在 Vue 3 应用中集成 WebGL 可以创建复杂的视觉效果、数据可视化、游戏和交互式 3D 体验,显著提升应用的视觉吸引力和性能。

核心知识

1. WebGL 基本概念

  • 作用:提供硬件加速的图形渲染能力
  • 基于:OpenGL ES 2.0/3.0
  • 渲染流程
    1. 创建 WebGL 上下文
    2. 编写着色器程序(顶点着色器和片元着色器)
    3. 设置顶点数据和缓冲区
    4. 配置纹理和材质
    5. 设置变换矩阵
    6. 执行绘制命令
  • 坐标系:右手坐标系,默认视口范围为 [-1, 1](x, y, z)

2. WebGL 着色器

  • 顶点着色器:处理顶点位置、变换和传递数据给片元着色器
  • 片元着色器:处理像素颜色计算
  • GLSL:OpenGL 着色语言,用于编写着色器程序

3. WebGL 高级特性

  • 帧缓冲对象(FBO):用于离屏渲染
  • 顶点数组对象(VAO):管理顶点属性状态
  • 实例化渲染:高效渲染大量相同的几何体
  • 变换反馈:将顶点着色器的输出写回缓冲区
  • 多重采样抗锯齿(MSAA):提高渲染质量
  • 纹理数组和 3D 纹理:处理复杂纹理数据
  • Uniform 缓冲区对象(UBO):高效共享着色器 uniforms

4. WebGL 性能优化

  • 减少绘制调用:使用批处理和实例化
  • 优化着色器:减少计算复杂度
  • 使用适当的缓冲区类型:ARRAY_BUFFER、ELEMENT_ARRAY_BUFFER
  • 纹理压缩:减少内存占用和带宽
  • 使用顶点数组对象:减少状态切换
  • 避免频繁的 uniform 更新:使用 UBO 或纹理存储数据
  • 合理设置视口和裁剪范围:减少不必要的渲染

前端实现(Vue 3)

4.1 基础 WebGL 渲染设置

<template>
  <div>
    <h2>Vue 3 WebGL 基础示例</h2>
    <canvas ref="canvas" width="800" height="600"></canvas>
    <div v-if="error" class="error">{{ error }}</div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';

const canvas = ref(null);
const error = ref('');

let gl = null;
let program = null;
let vertexBuffer = null;
let indexBuffer = null;
let positionAttributeLocation = null;
let colorUniformLocation = null;
let matrixUniformLocation = null;
let animationFrameId = null;

// 顶点着色器源码
const vertexShaderSource = `
  attribute vec3 a_position;
  uniform mat4 u_matrix;
  
  void main() {
    gl_Position = u_matrix * vec4(a_position, 1.0);
    gl_PointSize = 10.0;
  }
`;

// 片元着色器源码
const fragmentShaderSource = `
  precision mediump float;
  uniform vec4 u_color;
  
  void main() {
    gl_FragColor = u_color;
  }
`;

// 编译着色器
const compileShader = (gl, type, source) => {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const errorMsg = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error(`着色器编译失败: ${errorMsg}`);
  }
  
  return shader;
};

// 创建着色器程序
const 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)) {
    const errorMsg = gl.getProgramInfoLog(program);
    gl.deleteProgram(program);
    throw new Error(`着色器程序链接失败: ${errorMsg}`);
  }
  
  return program;
};

// 初始化 WebGL
const initWebGL = () => {
  try {
    // 获取 WebGL 上下文
    gl = canvas.value.getContext('webgl') || canvas.value.getContext('experimental-webgl');
    if (!gl) {
      throw new Error('无法获取 WebGL 上下文');
    }
    
    // 编译着色器
    const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
    
    // 创建着色器程序
    program = createProgram(gl, vertexShader, fragmentShader);
    
    // 获取属性和 uniform 位置
    positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
    colorUniformLocation = gl.getUniformLocation(program, 'u_color');
    matrixUniformLocation = gl.getUniformLocation(program, 'u_matrix');
    
    // 创建顶点缓冲区
    vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    
    // 顶点数据(一个三角形)
    const vertices = [
      0.0, 0.5, 0.0,
     -0.5, -0.5, 0.0,
      0.5, -0.5, 0.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    
    // 创建索引缓冲区
    indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    const indices = [0, 1, 2];
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
    
    // 启用顶点属性
    gl.enableVertexAttribArray(positionAttributeLocation);
    gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
    
    // 设置视口
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    
    // 清除颜色
    gl.clearColor(0.1, 0.1, 0.1, 1.0);
    
    // 启用深度测试
    gl.enable(gl.DEPTH_TEST);
    
  } catch (err) {
    error.value = err.message;
    console.error('初始化 WebGL 失败:', err);
  }
};

// 绘制函数
const draw = (time) => {
  if (!gl || !program) return;
  
  // 清除画布
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
  // 使用着色器程序
  gl.useProgram(program);
  
  // 设置颜色 uniform
  gl.uniform4f(colorUniformLocation, 0.0, 0.7, 1.0, 1.0);
  
  // 创建旋转矩阵
  const angle = time * 0.001;
  const cos = Math.cos(angle);
  const sin = Math.sin(angle);
  
  // 2D 旋转矩阵
  const matrix = [
    cos, -sin, 0, 0,
    sin, cos, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1
  ];
  
  gl.uniformMatrix4fv(matrixUniformLocation, false, matrix);
  
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  
  // 绘制三角形
  gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);
  
  // 请求下一帧
  animationFrameId = requestAnimationFrame(draw);
};

// 组件挂载时初始化
onMounted(() => {
  initWebGL();
  if (gl) {
    animationFrameId = requestAnimationFrame(draw);
  }
});

// 组件销毁前清理
onBeforeUnmount(() => {
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId);
  }
  
  if (gl) {
    if (program) {
      gl.deleteProgram(program);
    }
    if (vertexBuffer) {
      gl.deleteBuffer(vertexBuffer);
    }
    if (indexBuffer) {
      gl.deleteBuffer(indexBuffer);
    }
  }
});
</script>

<style scoped>
canvas {
  border: 1px solid #ddd;
  border-radius: 4px;
}

.error {
  color: red;
  margin: 1rem 0;
}
</style>

4.2 使用着色器渲染复杂图形

<template>
  <div>
    <h2>WebGL 复杂图形渲染</h2>
    <canvas ref="canvas" width="800" height="600"></canvas>
    <div class="controls">
      <label>旋转速度: {{ rotationSpeed }}</label>
      <input type="range" v-model.number="rotationSpeed" min="0" max="2" step="0.1" />
      <label>颜色: </label>
      <input type="color" v-model="color" />
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';

const canvas = ref(null);
const rotationSpeed = ref(0.5);
const color = ref('#0077ff');

let gl = null;
let program = null;
let vertexBuffer = null;
let indexBuffer = null;
let positionAttributeLocation = null;
let colorUniformLocation = null;
let matrixUniformLocation = null;
let timeUniformLocation = null;
let animationFrameId = null;
let startTime = 0;

// 顶点着色器源码(带变换和时间)
const vertexShaderSource = `
  attribute vec3 a_position;
  uniform mat4 u_matrix;
  uniform float u_time;
  
  void main() {
    // 添加正弦波动画
    vec3 animatedPosition = a_position;
    animatedPosition.y += sin(u_time + a_position.x * 10.0) * 0.1;
    
    gl_Position = u_matrix * vec4(animatedPosition, 1.0);
    gl_PointSize = 5.0;
  }
`;

// 片元着色器源码(带时间和颜色)
const fragmentShaderSource = `
  precision mediump float;
  uniform vec4 u_color;
  uniform float u_time;
  
  void main() {
    // 添加渐变效果
    vec4 gradientColor = u_color;
    gradientColor.a = 0.8 + sin(gl_FragCoord.x * 0.01 + u_time) * 0.2;
    
    gl_FragColor = gradientColor;
  }
`;

// 编译着色器
const compileShader = (gl, type, source) => {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    throw new Error(`着色器编译失败: ${gl.getShaderInfoLog(shader)}`);
  }
  
  return shader;
};

// 创建着色器程序
const 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)) {
    throw new Error(`着色器程序链接失败: ${gl.getProgramInfoLog(program)}`);
  }
  
  return program;
};

// 初始化 WebGL
const initWebGL = () => {
  gl = canvas.value.getContext('webgl');
  if (!gl) {
    throw new Error('无法获取 WebGL 上下文');
  }
  
  // 编译着色器
  const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
  const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
  
  // 创建着色器程序
  program = createProgram(gl, vertexShader, fragmentShader);
  
  // 获取属性和 uniform 位置
  positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
  colorUniformLocation = gl.getUniformLocation(program, 'u_color');
  matrixUniformLocation = gl.getUniformLocation(program, 'u_matrix');
  timeUniformLocation = gl.getUniformLocation(program, 'u_time');
  
  // 创建顶点缓冲区
  vertexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  
  // 生成一个圆的顶点数据
  const vertices = [];
  const segments = 50;
  const radius = 0.5;
  
  // 圆心
  vertices.push(0.0, 0.0, 0.0);
  
  // 圆周顶点
  for (let i = 0; i <= segments; i++) {
    const angle = (i / segments) * Math.PI * 2;
    vertices.push(
      Math.cos(angle) * radius,
      Math.sin(angle) * radius,
      0.0
    );
  }
  
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
  
  // 创建索引缓冲区
  indexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  
  const indices = [];
  for (let i = 1; i <= segments; i++) {
    indices.push(0, i, i + 1);
  }
  
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
  
  // 设置视口
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  
  // 清除颜色
  gl.clearColor(0.1, 0.1, 0.1, 1.0);
  
  // 启用混合
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
};

// 绘制函数
const draw = (timestamp) => {
  if (!gl || !program) return;
  
  if (!startTime) startTime = timestamp;
  const elapsedTime = (timestamp - startTime) * 0.001 * rotationSpeed.value;
  
  // 清除画布
  gl.clear(gl.COLOR_BUFFER_BIT);
  
  // 使用着色器程序
  gl.useProgram(program);
  
  // 设置颜色 uniform
  const rgb = hexToRgb(color.value);
  gl.uniform4f(colorUniformLocation, rgb.r / 255, rgb.g / 255, rgb.b / 255, 1.0);
  
  // 设置时间 uniform
  gl.uniform1f(timeUniformLocation, elapsedTime);
  
  // 创建旋转矩阵
  const angle = elapsedTime;
  const cos = Math.cos(angle);
  const sin = Math.sin(angle);
  
  const matrix = [
    cos, -sin, 0, 0,
    sin, cos, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1
  ];
  
  gl.uniformMatrix4fv(matrixUniformLocation, false, matrix);
  
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  
  // 启用并配置顶点属性
  gl.enableVertexAttribArray(positionAttributeLocation);
  gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
  
  // 绘制圆(三角形扇形)
  gl.drawElements(gl.TRIANGLES, 51 * 3, gl.UNSIGNED_SHORT, 0);
  
  // 请求下一帧
  animationFrameId = requestAnimationFrame(draw);
};

// 辅助函数:十六进制颜色转 RGB
const hexToRgb = (hex) => {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : { r: 0, g: 0, b: 0 };
};

// 组件挂载时初始化
onMounted(() => {
  try {
    initWebGL();
    animationFrameId = requestAnimationFrame(draw);
  } catch (error) {
    console.error('初始化失败:', error);
  }
});

// 组件销毁前清理
onBeforeUnmount(() => {
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId);
  }
  
  if (gl) {
    if (program) gl.deleteProgram(program);
    if (vertexBuffer) gl.deleteBuffer(vertexBuffer);
    if (indexBuffer) gl.deleteBuffer(indexBuffer);
  }
});
</script>

<style scoped>
canvas {
  border: 1px solid #ddd;
  border-radius: 4px;
}

.controls {
  margin: 1rem 0;
  display: flex;
  gap: 1rem;
  align-items: center;
}

label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
</style>

4.3 创建可复用的 WebGL Composable

// composables/useWebGL.js
import { ref, onMounted, onBeforeUnmount } from 'vue';

export function useWebGL(canvasRef, options = {}) {
  const error = ref('');
  const gl = ref(null);
  const programs = ref({});
  const buffers = ref({});
  const vertexArrays = ref({});
  const textures = ref({});
  
  let animationFrameId = null;
  let startTime = 0;
  let drawCallback = null;
  
  // 编译着色器
  const compileShader = (type, source) => {
    if (!gl.value) throw new Error('WebGL 上下文未初始化');
    
    const shader = gl.value.createShader(type);
    gl.value.shaderSource(shader, source);
    gl.value.compileShader(shader);
    
    if (!gl.value.getShaderParameter(shader, gl.value.COMPILE_STATUS)) {
      const errMsg = gl.value.getShaderInfoLog(shader);
      gl.value.deleteShader(shader);
      throw new Error(`着色器编译失败: ${errMsg}`);
    }
    
    return shader;
  };
  
  // 创建着色器程序
  const createProgram = (vertexShaderSource, fragmentShaderSource, name = 'default') => {
    if (!gl.value) throw new Error('WebGL 上下文未初始化');
    
    const vertexShader = compileShader(gl.value.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = compileShader(gl.value.FRAGMENT_SHADER, fragmentShaderSource);
    
    const program = gl.value.createProgram();
    gl.value.attachShader(program, vertexShader);
    gl.value.attachShader(program, fragmentShader);
    gl.value.linkProgram(program);
    
    if (!gl.value.getProgramParameter(program, gl.value.LINK_STATUS)) {
      const errMsg = gl.value.getProgramInfoLog(program);
      gl.value.deleteProgram(program);
      throw new Error(`程序链接失败: ${errMsg}`);
    }
    
    // 删除着色器,因为它们已经链接到程序中
    gl.value.deleteShader(vertexShader);
    gl.value.deleteShader(fragmentShader);
    
    programs.value[name] = program;
    return program;
  };
  
  // 创建缓冲区
  const createBuffer = (type, data, usage = gl.value.STATIC_DRAW, name = 'default') => {
    if (!gl.value) throw new Error('WebGL 上下文未初始化');
    
    const buffer = gl.value.createBuffer();
    gl.value.bindBuffer(type, buffer);
    gl.value.bufferData(type, data, usage);
    
    buffers.value[name] = { buffer, type };
    return buffer;
  };
  
  // 创建顶点数组对象(VAO)
  const createVertexArray = (name = 'default') => {
    if (!gl.value) throw new Error('WebGL 上下文未初始化');
    
    // 检查是否支持 VAO
    if (!gl.value.createVertexArray) {
      throw new Error('您的浏览器不支持顶点数组对象(VAO)');
    }
    
    const vao = gl.value.createVertexArray();
    vertexArrays.value[name] = vao;
    return vao;
  };
  
  // 创建纹理
  const createTexture = (name = 'default') => {
    if (!gl.value) throw new Error('WebGL 上下文未初始化');
    
    const texture = gl.value.createTexture();
    textures.value[name] = texture;
    return texture;
  };
  
  // 初始化 WebGL 上下文
  const initWebGL = () => {
    try {
      const canvas = canvasRef.value;
      if (!canvas) {
        throw new Error('Canvas 元素未找到');
      }
      
      // 获取 WebGL 上下文
      const context = canvas.getContext('webgl', options) || canvas.getContext('experimental-webgl', options);
      if (!context) {
        throw new Error('无法获取 WebGL 上下文');
      }
      
      gl.value = context;
      
      // 设置默认视口
      gl.value.viewport(0, 0, canvas.width, canvas.height);
      
      // 设置默认清除颜色
      gl.value.clearColor(0.1, 0.1, 0.1, 1.0);
      
      // 启用深度测试(如果需要)
      if (options.depthTest !== false) {
        gl.value.enable(gl.value.DEPTH_TEST);
      }
      
    } catch (err) {
      error.value = err.message;
      console.error('初始化 WebGL 失败:', err);
      throw err;
    }
  };
  
  // 设置绘制回调
  const onDraw = (callback) => {
    drawCallback = callback;
  };
  
  // 绘制循环
  const drawLoop = (timestamp) => {
    if (!gl.value || !drawCallback) return;
    
    if (!startTime) startTime = timestamp;
    const elapsedTime = (timestamp - startTime) * 0.001;
    
    // 清除画布
    gl.value.clear(gl.value.COLOR_BUFFER_BIT | (gl.value.DEPTH_BUFFER_BIT || 0));
    
    // 调用绘制回调
    drawCallback(elapsedTime, gl.value);
    
    // 请求下一帧
    animationFrameId = requestAnimationFrame(drawLoop);
  };
  
  // 开始渲染循环
  const startRenderLoop = () => {
    if (!gl.value) {
      throw new Error('WebGL 上下文未初始化');
    }
    
    if (animationFrameId) {
      cancelAnimationFrame(animationFrameId);
    }
    
    animationFrameId = requestAnimationFrame(drawLoop);
  };
  
  // 停止渲染循环
  const stopRenderLoop = () => {
    if (animationFrameId) {
      cancelAnimationFrame(animationFrameId);
      animationFrameId = null;
    }
  };
  
  // 删除资源
  const cleanup = () => {
    stopRenderLoop();
    
    if (!gl.value) return;
    
    // 删除程序
    Object.values(programs.value).forEach(program => {
      gl.value.deleteProgram(program);
    });
    
    // 删除缓冲区
    Object.values(buffers.value).forEach(({ buffer }) => {
      gl.value.deleteBuffer(buffer);
    });
    
    // 删除顶点数组对象
    Object.values(vertexArrays.value).forEach(vao => {
      gl.value.deleteVertexArray(vao);
    });
    
    // 删除纹理
    Object.values(textures.value).forEach(texture => {
      gl.value.deleteTexture(texture);
    });
    
    // 清空资源引用
    programs.value = {};
    buffers.value = {};
    vertexArrays.value = {};
    textures.value = {};
  };
  
  // 组件挂载时初始化
  onMounted(() => {
    initWebGL();
  });
  
  // 组件销毁前清理
  onBeforeUnmount(() => {
    cleanup();
  });
  
  return {
    error,
    gl,
    programs,
    buffers,
    vertexArrays,
    textures,
    compileShader,
    createProgram,
    createBuffer,
    createVertexArray,
    createTexture,
    onDraw,
    startRenderLoop,
    stopRenderLoop,
    cleanup
  };
}

4.4 使用 Composable 的示例

<template>
  <div>
    <h2>使用 WebGL Composable</h2>
    <canvas ref="canvas" width="800" height="600"></canvas>
    <div v-if="webgl.error" class="error">{{ webgl.error }}</div>
    <div class="controls">
      <label>旋转: {{ rotation }}</label>
      <input type="range" v-model.number="rotation" min="0" max="2" step="0.1" />
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { useWebGL } from './composables/useWebGL';

const canvas = ref(null);
const rotation = ref(1);

// 使用 WebGL Composable
const webgl = useWebGL(canvas, {
  alpha: true,
  antialias: true
});

// 顶点着色器
const vertexShader = `
  attribute vec3 a_position;
  uniform mat4 u_matrix;
  
  void main() {
    gl_Position = u_matrix * vec4(a_position, 1.0);
  }
`;

// 片元着色器
const fragmentShader = `
  precision mediump float;
  uniform vec4 u_color;
  
  void main() {
    gl_FragColor = u_color;
  }
`;

// 绘制回调
const draw = (elapsedTime, gl) => {
  const program = webgl.programs.value.default;
  if (!program) return;
  
  // 使用着色器程序
  gl.useProgram(program);
  
  // 获取属性和 uniform 位置
  const positionLocation = gl.getAttribLocation(program, 'a_position');
  const colorLocation = gl.getUniformLocation(program, 'u_color');
  const matrixLocation = gl.getUniformLocation(program, 'u_matrix');
  
  // 绑定缓冲区
  const vertexBuffer = webgl.buffers.value.vertices;
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  
  // 启用并配置顶点属性
  gl.enableVertexAttribArray(positionLocation);
  gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
  
  // 设置颜色
  gl.uniform4f(colorLocation, 0.0, 0.7, 1.0, 0.8);
  
  // 创建旋转矩阵
  const angle = elapsedTime * rotation.value;
  const cos = Math.cos(angle);
  const sin = Math.sin(angle);
  
  const matrix = [
    cos, -sin, 0, 0,
    sin, cos, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1
  ];
  
  gl.uniformMatrix4fv(matrixLocation, false, matrix);
  
  // 绘制
  gl.drawArrays(gl.TRIANGLES, 0, 3);
};

// 组件挂载时设置 WebGL
onMounted(() => {
  if (!webgl.gl.value) return;
  
  // 创建着色器程序
  webgl.createProgram(vertexShader, fragmentShader, 'default');
  
  // 创建顶点缓冲区
  const vertices = [
    0.0, 0.5, 0.0,
   -0.5, -0.5, 0.0,
    0.5, -0.5, 0.0
  ];
  
  webgl.createBuffer(
    webgl.gl.value.ARRAY_BUFFER,
    new Float32Array(vertices),
    webgl.gl.value.STATIC_DRAW,
    'vertices'
  );
  
  // 设置绘制回调
  webgl.onDraw(draw);
  
  // 开始渲染循环
  webgl.startRenderLoop();
});
</script>

<style scoped>
canvas {
  border: 1px solid #ddd;
  border-radius: 4px;
}

.error {
  color: red;
  margin: 1rem 0;
}

.controls {
  margin: 1rem 0;
  display: flex;
  gap: 1rem;
  align-items: center;
}
</style>

最佳实践

1. 性能优化

  • 减少绘制调用:使用批处理和实例化渲染
  • 优化着色器:减少计算复杂度,避免分支语句
  • 使用 VAO:减少状态切换开销
  • 纹理优化:使用适当的纹理格式和压缩
  • 内存管理:及时删除不再使用的资源
  • 使用 UBO:高效共享 uniform 数据
  • 避免频繁的 uniform 更新:将动态数据存储在纹理中

2. 资源管理

  • 集中管理资源:使用 Composable 统一管理程序、缓冲区和纹理
  • 懒加载资源:只在需要时创建和加载资源
  • 资源池化:复用频繁创建和销毁的资源
  • 清理资源:在组件销毁时释放所有 WebGL 资源

3. 错误处理

  • 检查 WebGL 上下文创建:处理不支持 WebGL 的情况
  • 验证着色器编译和程序链接:提供详细的错误信息
  • 检查缓冲区和纹理操作:验证操作是否成功
  • 使用 WebGL 调试扩展:在开发环境中启用调试

4. 跨浏览器兼容性

  • 使用前缀:处理不同浏览器的 WebGL 上下文名称
  • 检查扩展支持:在使用高级特性前检查扩展是否可用
  • 提供降级方案:为不支持 WebGL 的浏览器提供替代体验
  • 测试多种浏览器:在主流浏览器上测试渲染结果

5. 调试技巧

  • 使用浏览器开发者工具:Chrome DevTools 和 Firefox DevTools 提供 WebGL 调试功能
  • 启用 WebGL 调试扩展:如 WEBGL_debug_renderer_infoWEBGL_lose_context
  • 打印着色器编译错误:在开发环境中显示详细的错误信息
  • 使用帧调试器:分析每帧的渲染状态和绘制调用
  • 监控性能:使用 requestAnimationFrame 测量帧率

常见问题与解决方案

1. WebGL 上下文创建失败

  • 原因
    • 浏览器不支持 WebGL
    • 设备硬件不支持 WebGL
    • 浏览器设置禁用了 WebGL
    • 页面在非安全上下文(HTTP)中运行
  • 解决方案
    • 检查浏览器支持情况
    • 提供降级方案
    • 确保在 HTTPS 环境下运行
    • 检查浏览器设置

2. 着色器编译错误

  • 原因
    • GLSL 语法错误
    • 使用了不支持的特性
    • 变量类型不匹配
  • 解决方案
    • 检查 GLSL 语法
    • 验证变量类型
    • 测试着色器在线编译器
    • 显示详细的错误信息

3. 渲染结果不正确

  • 原因
    • 顶点数据错误
    • 矩阵变换错误
    • 着色器逻辑错误
    • 纹理坐标错误
  • 解决方案
    • 检查顶点数据
    • 验证矩阵计算
    • 简化着色器逻辑
    • 调试纹理采样

4. 性能问题

  • 原因
    • 过多的绘制调用
    • 复杂的着色器计算
    • 大量的顶点数据
    • 频繁的状态切换
  • 解决方案
    • 使用批处理和实例化
    • 优化着色器
    • 减少顶点数量
    • 使用 VAO 减少状态切换

5. 跨域纹理问题

  • 原因
    • 纹理资源来自不同的域,没有正确的 CORS 头
  • 解决方案
    • 设置正确的 CORS 头
    • 使用 crossOrigin 属性加载图像
    • 将纹理数据内联到代码中

高级学习资源

1. 官方文档

2. 深度教程

3. 相关库和工具

4. 示例项目

5. 视频教程

实践练习

1. 基础练习:创建简单的 WebGL 应用

  • 创建 Vue 3 应用,使用 WebGL 渲染一个彩色三角形
  • 实现三角形的旋转动画
  • 添加颜色控制滑块

2. 进阶练习:绘制复杂图形

  • 使用 WebGL 绘制一个圆和一个矩形
  • 实现图形的缩放和旋转
  • 添加鼠标交互,允许用户拖动图形

3. 高级练习:3D 模型渲染

  • 加载和渲染一个简单的 3D 模型(如立方体或球体)
  • 实现 3D 模型的旋转和缩放
  • 添加基本的光照效果

4. 综合练习:WebGL 粒子系统

  • 创建一个粒子系统,渲染大量粒子
  • 实现粒子的运动和生命周期
  • 添加颜色渐变和透明度效果
  • 优化性能,实现高效渲染

5. 挑战练习:WebGL 物理模拟

  • 实现一个简单的物理模拟(如重力或碰撞检测)
  • 使用 WebGL 渲染物理模拟结果
  • 添加用户交互,允许用户影响物理系统
  • 优化性能,确保实时渲染

总结

WebGL 为 Vue 3 应用提供了强大的硬件加速渲染能力,可以创建复杂的视觉效果和交互式 3D 体验。通过合理使用 WebGL 的高级特性,如着色器、缓冲区、纹理和实例化渲染,可以构建高性能的图形应用。掌握 WebGL 的实现原理和最佳实践,对于构建现代化、视觉吸引力强的 Vue 3 应用至关重要。

« 上一篇 Vue 3与WebXR API - 实现虚拟现实和增强现实体验的核心技术 下一篇 » Vue 3与WebAssembly集成 - 实现高性能计算的核心技术