实战案例
案例 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 的实际应用:
- 粒子系统:GPU 计算和大规模粒子渲染
- 3D 场景漫游:相机系统和交互控制
- 模型加载器:3D 模型解析和渲染
- 后处理效果:帧缓冲和多通道渲染
- 交互式地形:程序化生成和法线计算
学习建议
- 从简单到复杂:先理解基础概念,再实现复杂效果
- 多实践:动手实现每个案例,加深理解
- 性能优化:关注性能,学习优化技巧
- 参考开源项目:学习 Three.js 等优秀项目的实现
进阶方向
- WebGPU:下一代图形 API
- 物理引擎:碰撞检测、刚体动力学
- 阴影技术:阴影贴图、级联阴影
- PBR 渲染:基于物理的渲染
- VR/AR:WebXR API 应用