{T}

实战案例

案例 1:粒子系统

粒子系统是 WebGL 的经典应用,用于模拟火焰、烟雾、雨雪等效果。

粒子着色器

顶点着色器

glsl
#version 300 es

in vec3 a_Position;
in vec3 a_Velocity;
in float a_Life;

uniform mat4 u_MVPMatrix;
uniform float u_Time;

out float v_Life;

void main() {
  // 根据时间和速度计算位置
  vec3 position = a_Position + a_Velocity * u_Time;
  
  // 应用重力
  position.y -= 0.5 * u_Time * u_Time;
  
  gl_Position = u_MVPMatrix * vec4(position, 1.0);
  gl_PointSize = mix(10.0, 0.0, u_Time / a_Life);
  
  v_Life = 1.0 - u_Time / a_Life;
}

片段着色器

glsl
#version 300 es
precision mediump float;

in float v_Life;
out vec4 fragColor;

void main() {
  // 圆形粒子
  float dist = distance(gl_PointCoord, vec2(0.5));
  if (dist > 0.5) discard;
  
  // 渐变颜色
  vec3 color = mix(vec3(1.0, 0.3, 0.0), vec3(1.0, 0.8, 0.0), v_Life);
  float alpha = v_Life * (1.0 - dist * 2.0);
  
  fragColor = vec4(color, alpha);
}

粒子系统类

javascript
import { mat4 } from 'gl-matrix';

class ParticleSystem {
  constructor(gl, count = 1000) {
    this.gl = gl;
    this.count = count;
    
    this.particles = [];
    this.initParticles();
    this.initBuffers();
    this.initShaders();
  }
  
  initParticles() {
    for (let i = 0; i < this.count; i++) {
      this.particles.push({
        position: [0, 0, 0],
        velocity: [
          (Math.random() - 0.5) * 2,
          Math.random() * 3 + 1,
          (Math.random() - 0.5) * 2
        ],
        life: Math.random() * 2 + 1
      });
    }
  }
  
  initBuffers() {
    const positions = new Float32Array(this.count * 3);
    const velocities = new Float32Array(this.count * 3);
    const lives = new Float32Array(this.count);
    
    this.particles.forEach((p, i) => {
      positions[i * 3] = p.position[0];
      positions[i * 3 + 1] = p.position[1];
      positions[i * 3 + 2] = p.position[2];
      
      velocities[i * 3] = p.velocity[0];
      velocities[i * 3 + 1] = p.velocity[1];
      velocities[i * 3 + 2] = p.velocity[2];
      
      lives[i] = p.life;
    });
    
    this.positionBuffer = this.createBuffer(positions);
    this.velocityBuffer = this.createBuffer(velocities);
    this.lifeBuffer = this.createBuffer(lives);
  }
  
  createBuffer(data) {
    const buffer = this.gl.createBuffer();
    this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
    this.gl.bufferData(this.gl.ARRAY_BUFFER, data, this.gl.STATIC_DRAW);
    return buffer;
  }
  
  initShaders() {
    // 编译着色器代码(省略)
    this.program = createProgram(this.gl, vertexShaderSource, fragmentShaderSource);
  }
  
  render(mvpMatrix, time) {
    const gl = this.gl;
    
    gl.useProgram(this.program);
    
    // 设置属性
    this.setAttribute('a_Position', this.positionBuffer, 3);
    this.setAttribute('a_Velocity', this.velocityBuffer, 3);
    this.setAttribute('a_Life', this.lifeBuffer, 1);
    
    // 设置 uniform
    gl.uniformMatrix4fv(gl.getUniformLocation(this.program, 'u_MVPMatrix'), false, mvpMatrix);
    gl.uniform1f(gl.getUniformLocation(this.program, 'u_Time'), time);
    
    // 启用混合
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
    
    // 绘制粒子
    gl.drawArrays(gl.POINTS, 0, this.count);
    
    gl.disable(gl.BLEND);
  }
  
  setAttribute(name, buffer, size) {
    const gl = this.gl;
    const location = gl.getAttribLocation(this.program, name);
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.enableVertexAttribArray(location);
    gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
  }
}

案例 2:3D 场景漫游

实现一个可交互的 3D 场景,支持键盘和鼠标控制。

场景类

javascript
class Scene3D {
  constructor(canvas) {
    this.canvas = canvas;
    this.gl = canvas.getContext('webgl2');
    
    this.objects = [];
    this.camera = new OrbitCamera();
    
    this.init();
  }
  
  init() {
    const gl = this.gl;
    
    // 启用深度测试
    gl.enable(gl.DEPTH_TEST);
    
    // 设置清除颜色
    gl.clearColor(0.1, 0.1, 0.2, 1.0);
    
    // 创建场景对象
    this.createScene();
    
    // 设置事件监听
    this.setupEventListeners();
    
    // 开始渲染循环
    this.render();
  }
  
  createScene() {
    // 创建地面
    const ground = this.createGround();
    this.objects.push(ground);
    
    // 创建立方体
    const cube = this.createCube();
    this.objects.push(cube);
    
    // 创建光源
    this.light = {
      position: [5, 10, 5],
      color: [1, 1, 1]
    };
  }
  
  createGround() {
    const geometry = new PlaneGeometry(20, 20);
    const material = new PhongMaterial({
      color: [0.5, 0.5, 0.5],
      shininess: 10
    });
    
    return new Mesh(geometry, material);
  }
  
  createCube() {
    const geometry = new BoxGeometry(2, 2, 2);
    const material = new PhongMaterial({
      color: [0.8, 0.2, 0.2],
      shininess: 50
    });
    
    const cube = new Mesh(geometry, material);
    cube.position = [0, 1, 0];
    
    return cube;
  }
  
  setupEventListeners() {
    let isDragging = false;
    let lastX, lastY;
    
    this.canvas.addEventListener('mousedown', (e) => {
      isDragging = true;
      lastX = e.clientX;
      lastY = e.clientY;
    });
    
    this.canvas.addEventListener('mousemove', (e) => {
      if (!isDragging) return;
      
      const deltaX = e.clientX - lastX;
      const deltaY = e.clientY - lastY;
      
      this.camera.rotate(deltaX * 0.01, deltaY * 0.01);
      
      lastX = e.clientX;
      lastY = e.clientY;
    });
    
    this.canvas.addEventListener('mouseup', () => {
      isDragging = false;
    });
    
    this.canvas.addEventListener('wheel', (e) => {
      e.preventDefault();
      this.camera.zoom(e.deltaY > 0 ? 1.1 : 0.9);
    });
    
    // 键盘控制
    document.addEventListener('keydown', (e) => {
      const speed = 0.5;
      switch (e.key) {
        case 'w':
          this.camera.moveForward(speed);
          break;
        case 's':
          this.camera.moveBackward(speed);
          break;
        case 'a':
          this.camera.moveLeft(speed);
          break;
        case 'd':
          this.camera.moveRight(speed);
          break;
      }
    });
  }
  
  render() {
    const gl = this.gl;
    
    // 清除缓冲区
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    
    // 获取相机矩阵
    const viewMatrix = this.camera.getViewMatrix();
    const projectionMatrix = this.camera.getProjectionMatrix(
      this.canvas.width / this.canvas.height
    );
    
    // 渲染所有对象
    for (const object of this.objects) {
      object.render(gl, {
        viewMatrix,
        projectionMatrix,
        light: this.light,
        cameraPosition: this.camera.position
      });
    }
    
    requestAnimationFrame(() => this.render());
  }
}

轨道相机类

javascript
import { mat4 } from 'gl-matrix';

class OrbitCamera {
  constructor() {
    this.target = [0, 0, 0];
    this.radius = 10;
    this.theta = Math.PI / 4;
    this.phi = Math.PI / 4;
    
    this.position = [0, 0, 10];
    this.updatePosition();
  }
  
  updatePosition() {
    this.position = [
      this.target[0] + this.radius * Math.sin(this.phi) * Math.sin(this.theta),
      this.target[1] + this.radius * Math.cos(this.phi),
      this.target[2] + this.radius * Math.sin(this.phi) * Math.cos(this.theta)
    ];
  }
  
  rotate(deltaTheta, deltaPhi) {
    this.theta -= deltaTheta;
    this.phi = Math.max(0.1, Math.min(Math.PI - 0.1, this.phi - deltaPhi));
    this.updatePosition();
  }
  
  zoom(factor) {
    this.radius = Math.max(2, Math.min(50, this.radius * factor));
    this.updatePosition();
  }
  
  getViewMatrix() {
    const viewMatrix = mat4.create();
    mat4.lookAt(viewMatrix, this.position, this.target, [0, 1, 0]);
    return viewMatrix;
  }
  
  getProjectionMatrix(aspect) {
    const projectionMatrix = mat4.create();
    mat4.perspective(projectionMatrix, Math.PI / 4, aspect, 0.1, 1000);
    return projectionMatrix;
  }
}

案例 3:模型加载器

加载和渲染 OBJ 格式的 3D 模型。

OBJ 解析器

javascript
class OBJLoader {
  constructor() {
    this.vertices = [];
    this.normals = [];
    this.texCoords = [];
    this.indices = [];
  }
  
  parse(text) {
    const lines = text.split('\n');
    
    const tempVertices = [];
    const tempNormals = [];
    const tempTexCoords = [];
    
    const vertexMap = new Map();
    let indexOffset = 0;
    
    for (const line of lines) {
      const parts = line.trim().split(/\s+/);
      const type = parts[0];
      
      if (type === 'v') {
        // 顶点
        tempVertices.push([
          parseFloat(parts[1]),
          parseFloat(parts[2]),
          parseFloat(parts[3])
        ]);
      } else if (type === 'vn') {
        // 法线
        tempNormals.push([
          parseFloat(parts[1]),
          parseFloat(parts[2]),
          parseFloat(parts[3])
        ]);
      } else if (type === 'vt') {
        // 纹理坐标
        tempTexCoords.push([
          parseFloat(parts[1]),
          parseFloat(parts[2])
        ]);
      } else if (type === 'f') {
        // 面
        const faceVertices = [];
        
        for (let i = 1; i < parts.length; i++) {
          const indices = parts[i].split('/');
          const vertexKey = parts[i];
          
          if (vertexMap.has(vertexKey)) {
            faceVertices.push(vertexMap.get(vertexKey));
          } else {
            const index = indexOffset++;
            vertexMap.set(vertexKey, index);
            faceVertices.push(index);
            
            // 顶点位置
            const vIndex = parseInt(indices[0]) - 1;
            this.vertices.push(...tempVertices[vIndex]);
            
            // 纹理坐标
            if (indices[1]) {
              const tIndex = parseInt(indices[1]) - 1;
              this.texCoords.push(...tempTexCoords[tIndex]);
            }
            
            // 法线
            if (indices[2]) {
              const nIndex = parseInt(indices[2]) - 1;
              this.normals.push(...tempNormals[nIndex]);
            }
          }
        }
        
        // 三角化面
        for (let i = 1; i < faceVertices.length - 1; i++) {
          this.indices.push(faceVertices[0], faceVertices[i], faceVertices[i + 1]);
        }
      }
    }
    
    return {
      vertices: new Float32Array(this.vertices),
      normals: new Float32Array(this.normals),
      texCoords: new Float32Array(this.texCoords),
      indices: new Uint16Array(this.indices)
    };
  }
}

模型类

javascript
class Model {
  constructor(gl) {
    this.gl = gl;
    this.meshes = [];
  }
  
  async loadOBJ(url) {
    const response = await fetch(url);
    const text = await response.text();
    
    const loader = new OBJLoader();
    const data = loader.parse(text);
    
    // 创建网格
    const mesh = this.createMesh(data);
    this.meshes.push(mesh);
  }
  
  createMesh(data) {
    const gl = this.gl;
    
    // 创建 VAO
    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);
    
    // 顶点缓冲区
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, data.vertices, gl.STATIC_DRAW);
    gl.enableVertexAttribArray(0);
    gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
    
    // 法线缓冲区
    if (data.normals.length > 0) {
      const normalBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, data.normals, gl.STATIC_DRAW);
      gl.enableVertexAttribArray(1);
      gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
    }
    
    // 索引缓冲区
    const indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data.indices, gl.STATIC_DRAW);
    
    gl.bindVertexArray(null);
    
    return {
      vao,
      indexCount: data.indices.length
    };
  }
  
  render(program) {
    const gl = this.gl;
    
    gl.useProgram(program);
    
    for (const mesh of this.meshes) {
      gl.bindVertexArray(mesh.vao);
      gl.drawElements(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0);
      gl.bindVertexArray(null);
    }
  }
}

案例 4:后处理效果

实现景深、辉光、色彩校正等后处理效果。

后处理管线

javascript
class PostProcessing {
  constructor(gl, width, height) {
    this.gl = gl;
    this.width = width;
    this.height = height;
    
    this.framebuffers = [];
    this.textures = [];
    
    this.init();
  }
  
  init() {
    // 创建帧缓冲
    this.createFramebuffer();
    
    // 创建着色器
    this.brightnessShader = this.createShader(brightnessVS, brightnessFS);
    this.blurShader = this.createShader(blurVS, blurFS);
    this.compositeShader = this.createShader(compositeVS, compositeFS);
  }
  
  createFramebuffer() {
    const gl = this.gl;
    
    // 颜色纹理
    const colorTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, colorTexture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    
    // 深度渲染缓冲
    const depthBuffer = gl.createRenderbuffer();
    gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.width, this.height);
    
    // 帧缓冲
    const framebuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0);
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
    
    this.mainFramebuffer = framebuffer;
    this.mainTexture = colorTexture;
  }
  
  beginCapture() {
    const gl = this.gl;
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.mainFramebuffer);
    gl.viewport(0, 0, this.width, this.height);
  }
  
  endCapture() {
    const gl = this.gl;
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.viewport(0, 0, this.width, this.height);
  }
  
  applyBloom() {
    const gl = this.gl;
    
    // 1. 提取高亮区域
    gl.useProgram(this.brightnessShader);
    gl.bindTexture(gl.TEXTURE_2D, this.mainTexture);
    // ... 渲染到临时缓冲
    
    // 2. 高斯模糊
    gl.useProgram(this.blurShader);
    // ... 水平模糊和垂直模糊
    
    // 3. 合成最终图像
    gl.useProgram(this.compositeShader);
    gl.bindTexture(gl.TEXTURE_2D, this.mainTexture);
    // ... 混合原始图像和辉光
  }
}

辉光着色器

提取高亮区域

glsl
// brightnessFS.glsl
precision highp float;

uniform sampler2D u_Texture;
uniform float u_Threshold;

in vec2 v_TexCoord;
out vec4 fragColor;

void main() {
  vec4 color = texture(u_Texture, v_TexCoord);
  
  // 计算亮度
  float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
  
  // 提取高亮区域
  if (brightness > u_Threshold) {
    fragColor = color;
  } else {
    fragColor = vec4(0.0);
  }
}

高斯模糊

glsl
// blurFS.glsl
precision highp float;

uniform sampler2D u_Texture;
uniform vec2 u_Direction;

in vec2 v_TexCoord;
out vec4 fragColor;

void main() {
  vec2 texOffset = 1.0 / vec2(textureSize(u_Texture, 0));
  vec3 result = texture(u_Texture, v_TexCoord).rgb * 0.227027;
  
  // 高斯权重
  float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
  
  for (int i = 1; i < 5; i++) {
    vec2 offset = u_Direction * texOffset * float(i);
    result += texture(u_Texture, v_TexCoord + offset).rgb * weights[i];
    result += texture(u_Texture, v_TexCoord - offset).rgb * weights[i];
  }
  
  fragColor = vec4(result, 1.0);
}

合成

glsl
// compositeFS.glsl
precision highp float;

uniform sampler2D u_SceneTexture;
uniform sampler2D u_BloomTexture;
uniform float u_BloomStrength;

in vec2 v_TexCoord;
out vec4 fragColor;

void main() {
  vec3 sceneColor = texture(u_SceneTexture, v_TexCoord).rgb;
  vec3 bloomColor = texture(u_BloomTexture, v_TexCoord).rgb;
  
  // 混合
  vec3 result = sceneColor + bloomColor * u_BloomStrength;
  
  // 色调映射
  result = result / (result + vec3(1.0));
  
  // Gamma 校正
  result = pow(result, vec3(1.0 / 2.2));
  
  fragColor = vec4(result, 1.0);
}

案例 5:交互式地形

基于高度图生成可交互的 3D 地形。

地形生成器

javascript
class Terrain {
  constructor(gl, width, depth, resolution) {
    this.gl = gl;
    this.width = width;
    this.depth = depth;
    this.resolution = resolution;
    
    this.generateMesh();
  }
  
  generateMesh() {
    const vertices = [];
    const normals = [];
    const texCoords = [];
    const indices = [];
    
    const halfWidth = this.width / 2;
    const halfDepth = this.depth / 2;
    
    // 生成顶点网格
    for (let z = 0; z <= this.resolution; z++) {
      for (let x = 0; x <= this.resolution; x++) {
        const u = x / this.resolution;
        const v = z / this.resolution;
        
        const px = (u - 0.5) * this.width;
        const pz = (v - 0.5) * this.depth;
        
        // 高度(使用噪声函数或高度图)
        const height = this.getHeight(px, pz);
        
        vertices.push(px, height, pz);
        texCoords.push(u, v);
      }
    }
    
    // 生成索引
    for (let z = 0; z < this.resolution; z++) {
      for (let x = 0; x < this.resolution; x++) {
        const topLeft = z * (this.resolution + 1) + x;
        const topRight = topLeft + 1;
        const bottomLeft = (z + 1) * (this.resolution + 1) + x;
        const bottomRight = bottomLeft + 1;
        
        indices.push(topLeft, bottomLeft, topRight);
        indices.push(topRight, bottomLeft, bottomRight);
      }
    }
    
    // 计算法线
    this.calculateNormals(vertices, indices, normals);
    
    // 创建缓冲区
    this.createBuffers(vertices, normals, texCoords, indices);
  }
  
  getHeight(x, z) {
    // 简单的高度函数(可替换为 Perlin 噪声)
    return Math.sin(x * 0.1) * Math.cos(z * 0.1) * 2;
  }
  
  calculateNormals(vertices, indices, normals) {
    for (let i = 0; i < vertices.length; i++) {
      normals.push(0, 0, 0);
    }
    
    // 遍历所有三角形,累加法线
    for (let i = 0; i < indices.length; i += 3) {
      const i0 = indices[i] * 3;
      const i1 = indices[i + 1] * 3;
      const i2 = indices[i + 2] * 3;
      
      // 计算三角形法线
      const v0 = [vertices[i0], vertices[i0 + 1], vertices[i0 + 2]];
      const v1 = [vertices[i1], vertices[i1 + 1], vertices[i1 + 2]];
      const v2 = [vertices[i2], vertices[i2 + 1], vertices[i2 + 2]];
      
      const edge1 = subtract(v1, v0);
      const edge2 = subtract(v2, v0);
      const normal = cross(edge1, edge2);
      
      // 累加到顶点法线
      normals[i0] += normal[0];
      normals[i0 + 1] += normal[1];
      normals[i0 + 2] += normal[2];
      
      normals[i1] += normal[0];
      normals[i1 + 1] += normal[1];
      normals[i1 + 2] += normal[2];
      
      normals[i2] += normal[0];
      normals[i2 + 1] += normal[1];
      normals[i2 + 2] += normal[2];
    }
    
    // 归一化法线
    for (let i = 0; i < normals.length; i += 3) {
      const len = Math.sqrt(normals[i] ** 2 + normals[i + 1] ** 2 + normals[i + 2] ** 2);
      normals[i] /= len;
      normals[i + 1] /= len;
      normals[i + 2] /= len;
    }
  }
  
  render(program) {
    const gl = this.gl;
    
    gl.bindVertexArray(this.vao);
    gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_INT, 0);
    gl.bindVertexArray(null);
  }
}

小结

项目总结

本章通过 5 个实战案例,展示了 WebGL 的实际应用:

  1. 粒子系统:GPU 计算和大规模粒子渲染
  2. 3D 场景漫游:相机系统和交互控制
  3. 模型加载器:3D 模型解析和渲染
  4. 后处理效果:帧缓冲和多通道渲染
  5. 交互式地形:程序化生成和法线计算

学习建议

  1. 从简单到复杂:先理解基础概念,再实现复杂效果
  2. 多实践:动手实现每个案例,加深理解
  3. 性能优化:关注性能,学习优化技巧
  4. 参考开源项目:学习 Three.js 等优秀项目的实现

进阶方向

  • WebGPU:下一代图形 API
  • 物理引擎:碰撞检测、刚体动力学
  • 阴影技术:阴影贴图、级联阴影
  • PBR 渲染:基于物理的渲染
  • VR/AR:WebXR API 应用

参考资源