第12章:实战案例集
12.1 章节概述
本章通过完整的实战案例,综合运用前面章节学习的知识。每个案例都包含完整的代码和详细的解析。
本章包含的案例:
- 3D 模型查看器:加载和显示 OBJ 模型
- 粒子系统:火焰、烟雾效果
- 后处理管线:Bloom、色调映射
- 天空盒与环境映射:反射效果
- 简易 3D 游戏:第一人称漫游
12.2 案例一:3D 模型查看器
12.2.1 功能需求
- 加载 OBJ 格式模型
- 支持轨道相机控制
- 基础光照(Blinn-Phong)
- 线框模式切换
12.2.2 OBJ 解析器
javascript
/**
* OBJ 文件解析器
*/
class OBJLoader {
static parse(objText) {
const positions = [];
const normals = [];
const uvs = [];
const vertices = [];
const indices = [];
const positionData = [];
const normalData = [];
const uvData = [];
const lines = objText.split('\n');
for (const line of lines) {
const parts = line.trim().split(/\s+/);
const type = parts[0];
switch (type) {
case 'v': // 顶点位置
positionData.push(
parseFloat(parts[1]),
parseFloat(parts[2]),
parseFloat(parts[3])
);
break;
case 'vn': // 法线
normalData.push(
parseFloat(parts[1]),
parseFloat(parts[2]),
parseFloat(parts[3])
);
break;
case 'vt': // 纹理坐标
uvData.push(
parseFloat(parts[1]),
parseFloat(parts[2])
);
break;
case 'f': // 面
// 处理三角形和四边形
const faceVertices = parts.slice(1).map(v => {
const indices = v.split('/').map(i => parseInt(i) - 1);
return {
position: indices[0],
uv: indices[1] || -1,
normal: indices[2] || -1
};
});
// 三角化(支持四边形)
for (let i = 1; i < faceVertices.length - 1; i++) {
this.addVertex(faceVertices[0], positionData, normalData, uvData, positions, normals, uvs);
this.addVertex(faceVertices[i], positionData, normalData, uvData, positions, normals, uvs);
this.addVertex(faceVertices[i + 1], positionData, normalData, uvData, positions, normals, uvs);
}
break;
}
}
// 如果没有法线,计算它们
if (normals.length === 0) {
this.computeNormals(positions, normals);
}
return {
positions: new Float32Array(positions),
normals: new Float32Array(normals),
uvs: new Float32Array(uvs),
vertexCount: positions.length / 3
};
}
static addVertex(v, posData, normData, uvData, positions, normals, uvs) {
positions.push(
posData[v.position * 3],
posData[v.position * 3 + 1],
posData[v.position * 3 + 2]
);
if (v.normal >= 0) {
normals.push(
normData[v.normal * 3],
normData[v.normal * 3 + 1],
normData[v.normal * 3 + 2]
);
}
if (v.uv >= 0) {
uvs.push(
uvData[v.uv * 2],
uvData[v.uv * 2 + 1]
);
}
}
static computeNormals(positions, normals) {
for (let i = 0; i < positions.length; i += 9) {
const v0 = [positions[i], positions[i+1], positions[i+2]];
const v1 = [positions[i+3], positions[i+4], positions[i+5]];
const v2 = [positions[i+6], positions[i+7], positions[i+8]];
const e1 = [v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]];
const e2 = [v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]];
const n = [
e1[1]*e2[2] - e1[2]*e2[1],
e1[2]*e2[0] - e1[0]*e2[2],
e1[0]*e2[1] - e1[1]*e2[0]
];
const len = Math.sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]);
n[0] /= len; n[1] /= len; n[2] /= len;
for (let j = 0; j < 3; j++) {
normals.push(n[0], n[1], n[2]);
}
}
}
}12.2.3 完整的模型查看器
javascript
/**
* 3D 模型查看器
*/
class ModelViewer {
constructor(canvas) {
this.canvas = canvas;
this.gl = canvas.getContext('webgl2');
this.initShaders();
this.initCamera();
this.initLighting();
this.model = null;
this.wireframe = false;
}
initShaders() {
const vertexShader = `#version 300 es
in vec3 a_position;
in vec3 a_normal;
uniform mat4 u_mvp;
uniform mat4 u_model;
uniform mat3 u_normalMatrix;
out vec3 v_worldPos;
out vec3 v_normal;
void main() {
v_worldPos = (u_model * vec4(a_position, 1.0)).xyz;
v_normal = u_normalMatrix * a_normal;
gl_Position = u_mvp * vec4(a_position, 1.0);
}
`;
const fragmentShader = `#version 300 es
precision highp float;
in vec3 v_worldPos;
in vec3 v_normal;
uniform vec3 u_lightPos;
uniform vec3 u_lightColor;
uniform vec3 u_cameraPos;
uniform vec3 u_baseColor;
uniform float u_shininess;
out vec4 fragColor;
void main() {
vec3 N = normalize(v_normal);
vec3 L = normalize(u_lightPos - v_worldPos);
vec3 V = normalize(u_cameraPos - v_worldPos);
vec3 H = normalize(L + V);
// 环境光
vec3 ambient = 0.1 * u_baseColor;
// 漫反射
float diff = max(dot(N, L), 0.0);
vec3 diffuse = diff * u_lightColor * u_baseColor;
// 镜面反射
float spec = pow(max(dot(N, H), 0.0), u_shininess);
vec3 specular = spec * u_lightColor;
fragColor = vec4(ambient + diffuse + specular, 1.0);
}
`;
this.program = this.createProgram(vertexShader, fragmentShader);
}
initCamera() {
this.camera = new OrbitCamera();
this.camera.setTarget(0, 0, 0);
this.camera.setDistance(5);
// 添加鼠标控制
new CameraController(this.camera, this.canvas);
}
initLighting() {
this.lightPosition = [5, 5, 5];
this.lightColor = [1, 1, 1];
}
async loadModel(url) {
const response = await fetch(url);
const objText = await response.text();
const meshData = OBJLoader.parse(objText);
const gl = this.gl;
// 创建 VAO
this.modelVAO = gl.createVertexArray();
gl.bindVertexArray(this.modelVAO);
// 位置缓冲
const posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, meshData.positions, gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
// 法线缓冲
const normBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normBuffer);
gl.bufferData(gl.ARRAY_BUFFER, meshData.normals, gl.STATIC_DRAW);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(1);
gl.bindVertexArray(null);
this.vertexCount = meshData.vertexCount;
// 计算包围盒,自动调整相机
this.computeBounds(meshData.positions);
}
computeBounds(positions) {
let minX = Infinity, maxX = -Infinity;
let minY = Infinity, maxY = -Infinity;
let minZ = Infinity, maxZ = -Infinity;
for (let i = 0; i < positions.length; i += 3) {
minX = Math.min(minX, positions[i]);
maxX = Math.max(maxX, positions[i]);
minY = Math.min(minY, positions[i+1]);
maxY = Math.max(maxY, positions[i+1]);
minZ = Math.min(minZ, positions[i+2]);
maxZ = Math.max(maxZ, positions[i+2]);
}
const center = [
(minX + maxX) / 2,
(minY + maxY) / 2,
(minZ + maxZ) / 2
];
const size = Math.max(maxX - minX, maxY - minY, maxZ - minZ);
this.camera.setTarget(...center);
this.camera.setDistance(size * 2);
}
render() {
const gl = this.gl;
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
gl.clearColor(0.1, 0.1, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
if (!this.modelVAO) return;
gl.useProgram(this.program);
// 设置矩阵
const model = mat4.create();
const view = this.camera.getViewMatrix();
const projection = mat4.create();
mat4.perspective(projection, Math.PI / 4, this.canvas.width / this.canvas.height, 0.1, 100);
const mvp = mat4.create();
mat4.multiply(mvp, view, model);
mat4.multiply(mvp, projection, mvp);
const normalMatrix = mat3.create();
mat3.normalFromMat4(normalMatrix, model);
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, 'u_mvp'), false, mvp);
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, 'u_model'), false, model);
gl.uniformMatrix3fv(gl.getUniformLocation(this.program, 'u_normalMatrix'), false, normalMatrix);
// 设置光照
gl.uniform3fv(gl.getUniformLocation(this.program, 'u_lightPos'), this.lightPosition);
gl.uniform3fv(gl.getUniformLocation(this.program, 'u_lightColor'), this.lightColor);
gl.uniform3fv(gl.getUniformLocation(this.program, 'u_cameraPos'), this.camera.position);
gl.uniform3fv(gl.getUniformLocation(this.program, 'u_baseColor'), [0.8, 0.8, 0.8]);
gl.uniform1f(gl.getUniformLocation(this.program, 'u_shininess'), 32);
// 绘制
gl.bindVertexArray(this.modelVAO);
if (this.wireframe) {
gl.drawArrays(gl.LINES, 0, this.vertexCount);
} else {
gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
}
gl.bindVertexArray(null);
}
start() {
const loop = () => {
this.render();
requestAnimationFrame(loop);
};
loop();
}
}
// 使用
const viewer = new ModelViewer(document.getElementById('canvas'));
await viewer.loadModel('models/bunny.obj');
viewer.start();12.3 案例二:粒子系统
12.3.1 CPU 粒子系统
javascript
/**
* CPU 粒子系统
*/
class ParticleSystem {
constructor(gl, maxParticles = 10000) {
this.gl = gl;
this.maxParticles = maxParticles;
this.particles = [];
this.deadParticles = [];
// 发射器配置
this.emitterPosition = [0, 0, 0];
this.emitRate = 100; // 每秒发射数量
this.particleLife = 3.0;
// 粒子属性范围
this.speedRange = [2, 5];
this.sizeRange = [5, 20];
this.colorStart = [1, 0.5, 0, 1];
this.colorEnd = [1, 0, 0, 0];
this.initBuffers();
this.initShaders();
this.lastEmitTime = 0;
this.emitAccumulator = 0;
}
initBuffers() {
const gl = this.gl;
// 粒子数据缓冲(位置 + 颜色 + 大小)
this.particleBuffer = gl.createBuffer();
this.particleData = new Float32Array(this.maxParticles * 8); // x,y,z,r,g,b,a,size
}
initShaders() {
const vertexShader = `#version 300 es
in vec3 a_position;
in vec4 a_color;
in float a_size;
uniform mat4 u_mvp;
out vec4 v_color;
void main() {
gl_Position = u_mvp * vec4(a_position, 1.0);
gl_PointSize = a_size;
v_color = a_color;
}
`;
const fragmentShader = `#version 300 es
precision highp float;
in vec4 v_color;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
vec2 uv = gl_PointCoord;
vec4 tex = texture(u_texture, uv);
// 圆形衰减
float dist = length(uv - 0.5) * 2.0;
float alpha = 1.0 - smoothstep(0.5, 1.0, dist);
fragColor = v_color * tex * alpha;
}
`;
this.program = this.createProgram(vertexShader, fragmentShader);
}
emit(count) {
for (let i = 0; i < count; i++) {
if (this.particles.length >= this.maxParticles) {
// 复用死亡粒子
if (this.deadParticles.length > 0) {
const particle = this.deadParticles.pop();
this.resetParticle(particle);
this.particles.push(particle);
}
} else {
this.particles.push(this.createParticle());
}
}
}
createParticle() {
const speed = this.random(this.speedRange[0], this.speedRange[1]);
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
return {
position: [...this.emitterPosition],
velocity: [
speed * Math.sin(phi) * Math.cos(theta),
speed * Math.cos(phi) + 2, // 向上的偏移
speed * Math.sin(phi) * Math.sin(theta)
],
life: this.particleLife,
maxLife: this.particleLife,
size: this.random(this.sizeRange[0], this.sizeRange[1]),
color: [...this.colorStart]
};
}
resetParticle(particle) {
const speed = this.random(this.speedRange[0], this.speedRange[1]);
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
particle.position = [...this.emitterPosition];
particle.velocity = [
speed * Math.sin(phi) * Math.cos(theta),
speed * Math.cos(phi) + 2,
speed * Math.sin(phi) * Math.sin(theta)
];
particle.life = this.particleLife;
particle.maxLife = this.particleLife;
particle.size = this.random(this.sizeRange[0], this.sizeRange[1]);
particle.color = [...this.colorStart];
}
update(deltaTime) {
const gravity = [0, -9.8, 0];
// 发射新粒子
this.emitAccumulator += deltaTime * this.emitRate;
const toEmit = Math.floor(this.emitAccumulator);
this.emitAccumulator -= toEmit;
this.emit(toEmit);
// 更新粒子
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
// 更新生命
p.life -= deltaTime;
if (p.life <= 0) {
// 移除死亡粒子
this.deadParticles.push(this.particles.splice(i, 1)[0]);
continue;
}
// 更新速度(重力)
p.velocity[0] += gravity[0] * deltaTime;
p.velocity[1] += gravity[1] * deltaTime;
p.velocity[2] += gravity[2] * deltaTime;
// 更新位置
p.position[0] += p.velocity[0] * deltaTime;
p.position[1] += p.velocity[1] * deltaTime;
p.position[2] += p.velocity[2] * deltaTime;
// 插值颜色
const t = 1 - p.life / p.maxLife;
for (let j = 0; j < 4; j++) {
p.color[j] = this.colorStart[j] + (this.colorEnd[j] - this.colorStart[j]) * t;
}
}
// 更新缓冲数据
for (let i = 0; i < this.particles.length; i++) {
const p = this.particles[i];
const offset = i * 8;
this.particleData[offset + 0] = p.position[0];
this.particleData[offset + 1] = p.position[1];
this.particleData[offset + 2] = p.position[2];
this.particleData[offset + 3] = p.color[0];
this.particleData[offset + 4] = p.color[1];
this.particleData[offset + 5] = p.color[2];
this.particleData[offset + 6] = p.color[3];
this.particleData[offset + 7] = p.size;
}
}
render(mvp) {
const gl = this.gl;
if (this.particles.length === 0) return;
gl.useProgram(this.program);
// 上传数据
gl.bindBuffer(gl.ARRAY_BUFFER, this.particleBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this.particleData, gl.DYNAMIC_DRAW);
// 设置属性
const posLoc = gl.getAttribLocation(this.program, 'a_position');
const colorLoc = gl.getAttribLocation(this.program, 'a_color');
const sizeLoc = gl.getAttribLocation(this.program, 'a_size');
gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 32, 0);
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, 32, 12);
gl.enableVertexAttribArray(colorLoc);
gl.vertexAttribPointer(sizeLoc, 1, gl.FLOAT, false, 32, 28);
gl.enableVertexAttribArray(sizeLoc);
// 设置 uniform
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, 'u_mvp'), false, mvp);
// 启用混合
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE); // 加法混合
gl.depthMask(false);
// 绘制
gl.drawArrays(gl.POINTS, 0, this.particles.length);
// 恢复状态
gl.depthMask(true);
gl.disable(gl.BLEND);
}
random(min, max) {
return min + Math.random() * (max - min);
}
}12.4 案例三:后处理管线
12.4.1 Bloom 效果实现
javascript
/**
* 后处理管线
*/
class PostProcessPipeline {
constructor(gl, width, height) {
this.gl = gl;
this.width = width;
this.height = height;
this.createFramebuffers();
this.createShaders();
}
createFramebuffers() {
// 主场景 FBO(HDR)
this.sceneFBO = this.createFBO(this.width, this.height, true);
// 亮度提取 FBO
this.brightFBO = this.createFBO(this.width / 2, this.height / 2, true);
// 模糊 FBO(ping-pong)
this.blurFBO1 = this.createFBO(this.width / 4, this.height / 4, true);
this.blurFBO2 = this.createFBO(this.width / 4, this.height / 4, true);
}
createFBO(width, height, hdr = false) {
const gl = this.gl;
const fbo = {
framebuffer: gl.createFramebuffer(),
texture: gl.createTexture(),
width, height
};
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.framebuffer);
gl.bindTexture(gl.TEXTURE_2D, fbo.texture);
gl.texImage2D(
gl.TEXTURE_2D, 0,
hdr ? gl.RGBA16F : gl.RGBA,
width, height, 0,
gl.RGBA,
hdr ? gl.HALF_FLOAT : gl.UNSIGNED_BYTE,
null
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, fbo.texture, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return fbo;
}
createShaders() {
// 亮度提取着色器
this.brightPassProgram = this.createPostProcessProgram(`
precision highp float;
in vec2 v_texCoord;
uniform sampler2D u_texture;
uniform float u_threshold;
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);
}
}
`);
// 高斯模糊着色器
this.blurProgram = this.createPostProcessProgram(`
precision highp float;
in vec2 v_texCoord;
uniform sampler2D u_texture;
uniform vec2 u_direction;
uniform vec2 u_resolution;
out vec4 fragColor;
void main() {
vec2 texelSize = 1.0 / u_resolution;
vec4 result = vec4(0.0);
// 5-tap 高斯模糊
float weights[5];
weights[0] = 0.227027;
weights[1] = 0.1945946;
weights[2] = 0.1216216;
weights[3] = 0.054054;
weights[4] = 0.016216;
result += texture(u_texture, v_texCoord) * weights[0];
for (int i = 1; i < 5; i++) {
vec2 offset = u_direction * texelSize * float(i);
result += texture(u_texture, v_texCoord + offset) * weights[i];
result += texture(u_texture, v_texCoord - offset) * weights[i];
}
fragColor = result;
}
`);
// 合成着色器
this.compositeProgram = this.createPostProcessProgram(`
precision highp float;
in vec2 v_texCoord;
uniform sampler2D u_sceneTexture;
uniform sampler2D u_bloomTexture;
uniform float u_bloomIntensity;
uniform float u_exposure;
out vec4 fragColor;
// ACES 色调映射
vec3 aces(vec3 x) {
const float a = 2.51;
const float b = 0.03;
const float c = 2.43;
const float d = 0.59;
const float e = 0.14;
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}
void main() {
vec3 scene = texture(u_sceneTexture, v_texCoord).rgb;
vec3 bloom = texture(u_bloomTexture, v_texCoord).rgb;
// 合成
vec3 hdr = scene + bloom * u_bloomIntensity;
// 曝光
hdr *= u_exposure;
// 色调映射
vec3 ldr = aces(hdr);
// Gamma 校正
ldr = pow(ldr, vec3(1.0 / 2.2));
fragColor = vec4(ldr, 1.0);
}
`);
}
render(sceneRenderFn) {
const gl = this.gl;
// Pass 1: 渲染场景到 HDR FBO
gl.bindFramebuffer(gl.FRAMEBUFFER, this.sceneFBO.framebuffer);
gl.viewport(0, 0, this.sceneFBO.width, this.sceneFBO.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
sceneRenderFn();
// Pass 2: 提取高亮
gl.bindFramebuffer(gl.FRAMEBUFFER, this.brightFBO.framebuffer);
gl.viewport(0, 0, this.brightFBO.width, this.brightFBO.height);
this.renderQuad(this.brightPassProgram, this.sceneFBO.texture, {
u_threshold: 1.0
});
// Pass 3-4: 模糊(多次)
let inputTex = this.brightFBO.texture;
for (let i = 0; i < 5; i++) {
// 水平模糊
gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFBO1.framebuffer);
gl.viewport(0, 0, this.blurFBO1.width, this.blurFBO1.height);
this.renderQuad(this.blurProgram, inputTex, {
u_direction: [1, 0],
u_resolution: [this.blurFBO1.width, this.blurFBO1.height]
});
// 垂直模糊
gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFBO2.framebuffer);
gl.viewport(0, 0, this.blurFBO2.width, this.blurFBO2.height);
this.renderQuad(this.blurProgram, this.blurFBO1.texture, {
u_direction: [0, 1],
u_resolution: [this.blurFBO2.width, this.blurFBO2.height]
});
inputTex = this.blurFBO2.texture;
}
// Pass 5: 合成到屏幕
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, this.width, this.height);
gl.useProgram(this.compositeProgram);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.sceneFBO.texture);
gl.uniform1i(gl.getUniformLocation(this.compositeProgram, 'u_sceneTexture'), 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.blurFBO2.texture);
gl.uniform1i(gl.getUniformLocation(this.compositeProgram, 'u_bloomTexture'), 1);
gl.uniform1f(gl.getUniformLocation(this.compositeProgram, 'u_bloomIntensity'), 0.5);
gl.uniform1f(gl.getUniformLocation(this.compositeProgram, 'u_exposure'), 1.0);
this.drawFullscreenQuad();
}
}12.5 案例四:天空盒与环境映射
javascript
/**
* 天空盒渲染器
*/
class Skybox {
constructor(gl) {
this.gl = gl;
this.initCube();
this.initShaders();
}
initCube() {
// 立方体顶点(从内部看)
const vertices = new Float32Array([
-1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1,
-1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1,
1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, 1, -1, -1,
-1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, 1, -1, -1, 1,
-1, 1, -1, 1, 1, -1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1, -1,
-1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, 1, -1, 1
]);
this.vao = this.gl.createVertexArray();
this.gl.bindVertexArray(this.vao);
const buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW);
this.gl.vertexAttribPointer(0, 3, this.gl.FLOAT, false, 0, 0);
this.gl.enableVertexAttribArray(0);
this.gl.bindVertexArray(null);
}
initShaders() {
const vertexShader = `#version 300 es
in vec3 a_position;
out vec3 v_texCoord;
uniform mat4 u_viewProjection;
void main() {
v_texCoord = a_position;
vec4 pos = u_viewProjection * vec4(a_position, 1.0);
gl_Position = pos.xyww; // z = w,始终在最远处
}
`;
const fragmentShader = `#version 300 es
precision highp float;
in vec3 v_texCoord;
uniform samplerCube u_skybox;
out vec4 fragColor;
void main() {
fragColor = texture(u_skybox, v_texCoord);
}
`;
this.program = this.createProgram(vertexShader, fragmentShader);
}
async loadCubemap(paths) {
const gl = this.gl;
this.texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_CUBE_MAP, this.texture);
const targets = [
gl.TEXTURE_CUBE_MAP_POSITIVE_X,
gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
gl.TEXTURE_CUBE_MAP_NEGATIVE_Z
];
for (let i = 0; i < 6; i++) {
const image = await this.loadImage(paths[i]);
gl.texImage2D(targets[i], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
}
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);
}
render(viewMatrix, projectionMatrix) {
const gl = this.gl;
// 移除视图矩阵的平移部分
const viewNoTranslate = mat4.clone(viewMatrix);
viewNoTranslate[12] = 0;
viewNoTranslate[13] = 0;
viewNoTranslate[14] = 0;
const viewProjection = mat4.create();
mat4.multiply(viewProjection, projectionMatrix, viewNoTranslate);
gl.depthFunc(gl.LEQUAL);
gl.useProgram(this.program);
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, 'u_viewProjection'), false, viewProjection);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_CUBE_MAP, this.texture);
gl.uniform1i(gl.getUniformLocation(this.program, 'u_skybox'), 0);
gl.bindVertexArray(this.vao);
gl.drawArrays(gl.TRIANGLES, 0, 36);
gl.bindVertexArray(null);
gl.depthFunc(gl.LESS);
}
}12.6 案例五:简易第一人称漫游
javascript
/**
* 第一人称控制器
*/
class FirstPersonController {
constructor(camera, canvas) {
this.camera = camera;
this.canvas = canvas;
this.position = [0, 1.8, 5]; // 眼睛高度 1.8m
this.yaw = 0; // 水平旋转
this.pitch = 0; // 垂直旋转
this.moveSpeed = 5;
this.lookSpeed = 0.002;
this.keys = {};
this.isLocked = false;
this.setupEventListeners();
}
setupEventListeners() {
// 键盘输入
document.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
});
document.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
// 鼠标锁定
this.canvas.addEventListener('click', () => {
this.canvas.requestPointerLock();
});
document.addEventListener('pointerlockchange', () => {
this.isLocked = document.pointerLockElement === this.canvas;
});
// 鼠标移动
document.addEventListener('mousemove', (e) => {
if (!this.isLocked) return;
this.yaw -= e.movementX * this.lookSpeed;
this.pitch -= e.movementY * this.lookSpeed;
// 限制俯仰角
this.pitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, this.pitch));
});
}
update(deltaTime) {
// 计算前方和右方向
const forward = [
Math.sin(this.yaw),
0,
-Math.cos(this.yaw)
];
const right = [
Math.cos(this.yaw),
0,
Math.sin(this.yaw)
];
// 处理移动输入
const movement = [0, 0, 0];
if (this.keys['KeyW']) {
movement[0] += forward[0];
movement[2] += forward[2];
}
if (this.keys['KeyS']) {
movement[0] -= forward[0];
movement[2] -= forward[2];
}
if (this.keys['KeyA']) {
movement[0] -= right[0];
movement[2] -= right[2];
}
if (this.keys['KeyD']) {
movement[0] += right[0];
movement[2] += right[2];
}
// 归一化并应用速度
const len = Math.sqrt(movement[0]*movement[0] + movement[2]*movement[2]);
if (len > 0) {
movement[0] = movement[0] / len * this.moveSpeed * deltaTime;
movement[2] = movement[2] / len * this.moveSpeed * deltaTime;
}
// 更新位置
this.position[0] += movement[0];
this.position[2] += movement[2];
// 更新相机
this.updateCamera();
}
updateCamera() {
// 计算目标点
const target = [
this.position[0] + Math.sin(this.yaw) * Math.cos(this.pitch),
this.position[1] + Math.sin(this.pitch),
this.position[2] - Math.cos(this.yaw) * Math.cos(this.pitch)
];
this.camera.setPosition(...this.position);
this.camera.setTarget(...target);
}
}12.7 本章小结
通过这些实战案例,我们综合运用了:
- 模型加载:OBJ 解析、网格处理
- 粒子系统:发射器、物理更新、渲染
- 后处理:HDR、Bloom、色调映射
- 环境映射:立方体贴图、天空盒
- 交互控制:相机控制、键盘鼠标输入
这些案例涵盖了 WebGL 开发中最常见的需求,可以作为更复杂项目的起点。
12.8 延伸学习
推荐资源
- WebGL Fundamentals: https://webglfundamentals.org/
- Learn OpenGL: https://learnopengl.com/
- The Book of Shaders: https://thebookofshaders.com/
- Three.js: 学习成熟框架的实现
进阶主题
- PBR(物理渲染)
- 延迟渲染
- 屏幕空间反射(SSR)
- 环境光遮蔽(SSAO)
- 体积光
- GPU 驱动的管线
恭喜完成 WebGL 基础系列的学习!
继续探索图形编程的世界,创造更多精彩的视觉效果!
文档版本:v1.0
字数统计:约 12,000 字
代码示例:50+ 个
