{T}

WebGL2与高级渲染

目录


A. WebGL 2.0 新特性概览

本册正文基于 WebGL 1.0 讲解,本附录概述 WebGL 2.0 的核心新特性,帮助读者了解升级方向。

WebGL 2 vs WebGL 1 全局对比

图表渲染中…

核心新特性详解

1. GLSL 300 es

WebGL 2 使用 #version 300 es 着色器版本,语法变化显著:

对比项WebGL 1 (GLSL 100)WebGL 2 (GLSL 300 es)
版本声明无需声明#version 300 es (必须第一行)
顶点输出varyingout
片元输入varyingin
片元输出gl_FragColor自定义 out vec4 fragColor
纹理采样texture2D() / textureCube()统一 texture()
属性声明attribute / varyingin / out
循环限制编译时需可确定迭代次数动态循环 ✅
整数运算受限完整整数支持 ✅
位运算不支持支持 ✅

GLSL 300 es 着色器示例:

glsl
// 顶点着色器
#version 300 es
layout(location = 0) in vec3 aPosition;  // 使用 layout 限定符
layout(location = 1) in vec2 aTexCoord;

out vec2 vTexCoord;  // 替代 varying

uniform mat4 uMVP;

void main() {
    vTexCoord = aTexCoord;
    gl_Position = uMVP * vec4(aPosition, 1.0);
}
glsl
// 片元着色器
#version 300 es
precision highp float;

in vec2 vTexCoord;  // 替代 varying
out vec4 fragColor; // 替代 gl_FragColor

uniform sampler2D uTexture;

void main() {
    fragColor = texture(uTexture, vTexCoord); // 统一 texture() 函数
}

2. 顶点数组对象 (VAO)

WebGL 1 中每次绘制前都需要重新绑定缓冲区、设置属性指针。WebGL 2 的 VAO 将这些状态打包保存,一次设置、反复使用。

图表渲染中…
javascript
// WebGL 2 VAO 使用示例
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);

// 以下状态会被 VAO 记录
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);

gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(1);

gl.bindVertexArray(null); // 解绑

// --- 渲染循环中 ---
function render() {
    gl.bindVertexArray(vao);   // 一键恢复所有顶点属性状态
    gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
    gl.bindVertexArray(null);
}

3. 多渲染目标 (MRT)

WebGL 2 允许片元着色器同时输出到多个颜色附件,这对延迟渲染、后处理等高级技术至关重要。

glsl
// 片元着色器 — 多输出
#version 300 es
precision highp float;

layout(location = 0) out vec4 fragColor;      // 输出到 COLOR_ATTACHMENT0
layout(location = 1) out vec4 fragNormal;     // 输出到 COLOR_ATTACHMENT1
layout(location = 2) out vec4 fragPosition;   // 输出到 COLOR_ATTACHMENT2

void main() {
    fragColor = vec4(diffuseColor, 1.0);
    fragNormal = vec4(normal * 0.5 + 0.5, 1.0);
    fragPosition = vec4(worldPosition, 1.0);
}

4. 3D 纹理与整数纹理

纹理类型WebGL 1WebGL 2典型用途
2D 纹理普通贴图
立方体纹理天空盒、环境映射
3D 纹理体积渲染、噪声、切片数据
2D 纹理数组地形图层、精灵动画
整数纹理索引查找、精确计数

5. Transform Feedback

允许将顶点着色器的输出捕获到缓冲区,实现 GPU 端的数据循环处理(粒子系统、GPU 动画)。

图表渲染中…

6. 其他重要新特性

特性说明应用场景
实例化渲染gl.drawArraysInstanced()大量相同物体(草地、粒子)
多重采样MSAA 离屏渲染抗锯齿后处理
查询对象gl.beginQuery()遮挡查询、性能统计
采样器对象gl.createSampler()纹理参数与纹理数据解耦
Uniform Buffergl.uniformBlockBinding()批量传递 uniform,减少状态切换
非 2 的幂纹理完整支持任意尺寸纹理无限制

浏览器兼容性

浏览器WebGL 1WebGL 2备注
Chrome 56+完整支持
Firefox 51+完整支持
Safari 15+2021 年后完整支持
Edge 79+Chromium 内核后完整支持
iOS Safari 15+完整支持
Android Chrome⚠️部分设备需手动启用

[兼容性检测] 使用以下代码检测 WebGL 2 支持:

javascript
const gl2 = canvas.getContext('webgl2');
if (!gl2) {
    console.warn('WebGL 2 不可用,降级到 WebGL 1');
    gl = canvas.getContext('webgl');
}

迁移建议

图表渲染中…

[迁移原则] WebGL 2 完全向后兼容 WebGL 1。你可以先获取 webgl2 上下文,但继续使用 GLSL 100 着色器,再逐步升级到 GLSL 300 es。


B. 后处理与高级渲染技术

本册第 30 章介绍了帧缓冲的基本用法,本附录扩展讲解基于 FBO 的后处理技术和其他高级渲染方法。

后处理技术概览

图表渲染中…

1. 模糊效果 (Blur)

模糊是最基础的后处理,也是 Bloom 的前置步骤。常见算法:

模糊类型原理采样次数效果
盒式模糊 (Box Blur)对周围 N×N 像素取均值N²(如 9×9=81)均匀模糊,有方块感
高斯模糊 (Gaussian Blur)按高斯分布加权平均可分离为两 Pass(水平+垂直)✅ 自然柔和
** Kawase Blur**逐级扩大采样偏移每级 5 次采样高效近似高斯

高斯模糊分离实现(推荐):

图表渲染中…
glsl
// 高斯模糊片元着色器(水平 Pass)
#version 300 es
precision highp float;

in vec2 vTexCoord;
out vec4 fragColor;

uniform sampler2D uTexture;
uniform vec2 uResolution;  // 纹理尺寸

// 高斯权重 (σ ≈ 4)
const float weights[5] = float[](0.227, 0.194, 0.122, 0.061, 0.024);

void main() {
    vec2 texOffset = 1.0 / uResolution;  // 一个像素的纹理偏移

    vec4 result = texture(uTexture, vTexCoord) * weights[0];

    // 正负偏移各采样 4 个像素
    for (int i = 1; i < 5; i++) {
        result += texture(uTexture, vTexCoord + vec2(texOffset.x * i, 0.0)) * weights[i];
        result += texture(uTexture, vTexCoord - vec2(texOffset.x * i, 0.0)) * weights[i];
    }

    fragColor = result;
}

[性能优化] 高斯模糊的核心优化是分离性——将二维模糊分解为水平+垂直两个一维 Pass。总采样次数从 N² 降低到 2N。

2. 泛光效果 (Bloom)

Bloom 让场景中高亮区域产生"光晕溢出"效果,是提升视觉品质的关键后处理。

图表渲染中…
glsl
// 亮度提取着色器
precision highp float;

varying vec2 vTexCoord;
uniform sampler2D uScene;
uniform float uThreshold; // 亮度阈值,通常 0.8-1.2

void main() {
    vec4 color = texture2D(uScene, vTexCoord);
    float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); // 加权亮度

    if (brightness > uThreshold) {
        gl_FragColor = color;
    } else {
        gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
}
glsl
// 最终合成着色器
precision highp float;

varying vec2 vTexCoord;
uniform sampler2D uScene;     // 原始场景纹理
uniform sampler2D uBloom;     // 模糊后的亮部纹理

void main() {
    vec4 sceneColor = texture2D(uScene, vTexCoord);
    vec4 bloomColor = texture2D(uBloom, vTexCoord);

    // 叠加 Bloom 效果
    gl_FragColor = sceneColor + bloomColor * 1.5;
}

3. 边缘检测 (Edge Detection)

用于实现描边效果或卡通渲染的轮廓线。

算法梯度计算特点
Sobel水平 + 垂直 3×3 卷积核最常用,适中精度
Laplacian中心差分 + 四邻域简单,但对噪声敏感
Frei-Chen9 个基向量分解最精确,计算量最大
glsl
// Sobel 边缘检测着色器
precision highp float;

varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform vec2 uResolution;

void main() {
    vec2 texel = 1.0 / uResolution;

    // 采样 9 个邻域像素
    float tl = texture2D(uTexture, vTexCoord + vec2(-texel.x, texel.y)).r;
    float t  = texture2D(uTexture, vTexCoord + vec2(0.0, texel.y)).r;
    float tr = texture2D(uTexture, vTexCoord + vec2(texel.x, texel.y)).r;
    float l  = texture2D(uTexture, vTexCoord + vec2(-texel.x, 0.0)).r;
    float r  = texture2D(uTexture, vTexCoord + vec2(texel.x, 0.0)).r;
    float bl = texture2D(uTexture, vTexCoord + vec2(-texel.x, -texel.y)).r;
    float b  = texture2D(uTexture, vTexCoord + vec2(0.0, -texel.y)).r;
    float br = texture2D(uTexture, vTexCoord + vec2(texel.x, -texel.y)).r;

    // Sobel 水平梯度
    float gx = -tl - 2.0*l - bl + tr + 2.0*r + br;
    // Sobel 垂直梯度
    float gy = -tl - 2.0*t - tr + bl + 2.0*b + br;

    float edge = sqrt(gx * gx + gy * gy);
    gl_FragColor = vec4(vec3(edge), 1.0);
}

4. 抗锯齿后处理 (FXAA)

快速近似抗锯齿,仅 1 个 Pass,性能极佳。

抗锯齿方法Pass 数性能效果
MSAA(硬件)0(渲染时)⚠️ 显存消耗大✅ 最高质量
FXAA1✅ 最快⚠️ 可能模糊细节
SMAA3⚠️ 中等✅ 高质量

5. 色调映射 (Tone Mapping)

将 HDR 高动态范围颜色映射到 LDR 低动态范围,保留细节。

glsl
// Reinhard 色调映射(最简单)
vec3 reinhardToneMapping(vec3 color) {
    return color / (color + vec3(1.0));
}

// ACES 电影色调映射(行业标准)
vec3 acesToneMapping(vec3 x) {
    float a = 2.51;
    float b = 0.03;
    float c = 2.43;
    float d = 0.59;
    float e = 0.14;
    return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}

// 应用色调映射后还需要 Gamma 校正
vec3 gammaCorrection(vec3 color) {
    return pow(color, vec3(1.0 / 2.2)); // sRGB Gamma ≈ 2.2
}
色调映射算法特点适用场景
Reinhard简单,暗部可能丢失快速实现
ACES电影级,保留细节✅ 推荐生产使用
Uncharted 2游戏开发经典游戏场景
Exposure-based可控曝光参数需要交互调整

6. 后处理实现模板

javascript
// 后处理框架 — 通用模板
class PostProcessPipeline {
    constructor(gl) {
        this.gl = gl;
        this.passes = [];
    }

    addPass(shaderSource, uniforms) {
        this.passes.push({
            shader: this.createShaderProgram(shaderSource),
            fbo: this.createFBO(),
            uniforms: uniforms
        });
    }

    render(sceneTexture) {
        let inputTexture = sceneTexture;

        for (let i = 0; i < this.passes.length; i++) {
            const pass = this.passes[i];
            const isLast = i === this.passes.length - 1;

            // 最后一个 Pass 输出到屏幕,其余输出到 FBO
            if (isLast) {
                this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
            } else {
                this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, pass.fbo.framebuffer);
            }

            this.gl.useProgram(pass.shader);
            this.gl.bindTexture(this.gl.TEXTURE_2D, inputTexture);

            // 设置 uniform
            for (const [name, value] of Object.entries(pass.uniforms)) {
                this.setUniform(pass.shader, name, value);
            }

            // 绘制全屏四边形
            this.drawFullScreenQuad();

            // 下一 Pass 的输入是当前 Pass 的输出
            inputTexture = pass.fbo.texture;
        }
    }
}

[关键理解] 所有后处理都遵循同一模式:场景 → FBO → 后处理着色器 → 屏幕。区别仅在于着色器的算法不同。掌握这个框架后,你可以轻松组合多种后处理效果。


C. 3D模型加载与glTF格式

本册通过程序化生成几何体(立方体、球体等),本附录补充讲解如何加载外部 3D 模型文件。

为什么需要加载外部模型?

方式适用场景优点缺点
程序化生成简单规则形体(球、立方体)无需外部资源无法表示复杂模型
加载外部模型角色、建筑、道具等复杂形体丰富的艺术细节需要模型文件、加载时间

3D 模型格式对比

图表渲染中…
格式扩展名类型包含内容WebGL 支持推荐度
glTF.gltf / .glbJSON + 二进制几何、材质、动画、层级、相机✅ 最佳⭐⭐⭐⭐⭐
OBJ.obj纯文本几何、材质(需 .mtl)需转换⭐⭐
FBX.fbx二进制私有几何、材质、动画、骨骼需转换⭐⭐⭐
STL.stl文本/二进制仅几何需转换
COLLADA.daeXML几何、材质、动画需转换⭐⭐

glTF 格式详解

glTF (GL Transmission Format) 是 Khronos Group 制定的 Web 3D 标准格式,被业界称为"3D 的 JPEG"。

glTF 文件结构

图表渲染中…

glTF 的两种存储方式

方式扩展名数据存储优点缺点
标准 glTF.gltfJSON + 外部 .bin + 图片可读、易调试多文件,管理复杂
Binary glTF.glb单个二进制文件✅ 单文件、传输高效不可直接读取

glTF JSON 示例

json
{
  "asset": { "version": "2.0", "generator": "Example Generator" },
  "scene": 0,
  "scenes": [
    { "nodes": [0] }
  ],
  "nodes": [
    { "mesh": 0, "name": "Cube" }
  ],
  "meshes": [
    {
      "primitives": [
        {
          "attributes": {
            "POSITION": 1,
            "NORMAL": 2,
            "TEXCOORD_0": 3
          },
          "indices": 0,
          "material": 0
        }
      ]
    }
  ],
  "accessors": [
    { "bufferView": 0, "componentType": 5123, "count": 36, "type": "SCALAR", "max": [23], "min": [0] },
    { "bufferView": 1, "componentType": 5126, "count": 24, "type": "VEC3", "max": [1,1,1], "min": [-1,-1,-1] }
  ],
  "bufferViews": [
    { "buffer": 0, "byteOffset": 0, "byteLength": 72, "target": 34963 },
    { "buffer": 0, "byteOffset": 72, "byteLength": 288, "target": 34962 }
  ],
  "buffers": [
    { "uri": "cube.bin", "byteLength": 360 }
  ],
  "materials": [
    {
      "pbrMetallicRoughness": {
        "baseColorTexture": { "index": 0 },
        "metallicFactor": 0.0,
        "roughnessFactor": 1.0
      }
    }
  ]
}

手动解析 glTF 的核心步骤

图表渲染中…

使用加载库

手动解析 glTF 非常复杂,实际开发中推荐使用成熟的加载库:

特点适用场景
Three.js GLTFLoader功能最完善、社区最大通用开发、快速原型
Babylon.js商业级引擎、功能全面大型 3D 应用
glTF-Transform模型优化工具模型压缩、DRACO 解码
FilamentGoogle 的轻量渲染引擎移动端高性能渲染

Three.js 加载示例

javascript
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';

const loader = new GLTFLoader();

// 可选:配置 DRACO 解码器(压缩模型)
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
loader.setDRACOLoader(dracoLoader);

loader.load(
    'model.glb',       // glTF 文件路径
    (gltf) => {        // 加载成功回调
        const model = gltf.scene;
        scene.add(model);

        // 播放动画
        if (gltf.animations.length > 0) {
            const mixer = new THREE.AnimationMixer(model);
            const action = mixer.clipAction(gltf.animations[0]);
            action.play();
        }
    },
    (progress) => {    // 加载进度回调
        console.log(`Loading: ${(progress.loaded / progress.total * 100).toFixed(1)}%`);
    },
    (error) => {       // 加载错误回调
        console.error('Model loading failed:', error);
    }
);

简易 OBJ 加载器(WebGL 1 原生实现)

如果不想引入 Three.js,下面是一个简易 OBJ 解析器的核心逻辑:

javascript
function parseOBJ(text) {
    const positions = [];
    const normals = [];
    const texCoords = [];
    const faces = [];

    for (const line of text.split('\n')) {
        const parts = line.trim().split(/\s+/);
        const type = parts[0];

        if (type === 'v') {
            positions.push([parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3])]);
        } else if (type === 'vn') {
            normals.push([parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3])]);
        } else if (type === 'vt') {
            texCoords.push([parseFloat(parts[1]), parseFloat(parts[2])]);
        } else if (type === 'f') {
            // OBJ face format: v/vt/vn 或 v//vn 或 v
            const face = [];
            for (let i = 1; i < parts.length; i++) {
                const indices = parts[i].split('/');
                face.push({
                    position: parseInt(indices[0]) - 1,   // OBJ 索引从 1 开始
                    texCoord: indices[1] ? parseInt(indices[1]) - 1 : -1,
                    normal: indices[2] ? parseInt(indices[2]) - 1 : -1
                });
            }
            faces.push(face);
        }
    }

    // 将面数据展开为顶点数组
    const vertexPositions = [];
    const vertexNormals = [];
    const vertexTexCoords = [];
    const indices = [];
    const vertexMap = new Map();
    let nextIndex = 0;

    for (const face of faces) {
        for (const vertex of face) {
            const key = `${vertex.position}/${vertex.texCoord}/${vertex.normal}`;
            if (!vertexMap.has(key)) {
                vertexMap.set(key, nextIndex);
                vertexPositions.push(...positions[vertex.position]);
                if (vertex.normal >= 0) vertexNormals.push(...normals[vertex.normal]);
                if (vertex.texCoord >= 0) vertexTexCoords.push(...texCoords[vertex.texCoord]);
                nextIndex++;
            }
            indices.push(vertexMap.get(key));
        }
    }

    return {
        positions: new Float32Array(vertexPositions),
        normals: vertexNormals.length ? new Float32Array(vertexNormals) : null,
        texCoords: vertexTexCoords.length ? new Float32Array(vertexTexCoords) : null,
        indices: new Uint16Array(indices)
    };
}

模型优化建议

优化手段工具效果
DRACO 压缩glTF-Transform / Blender网格体积减少 80-95%
纹理压缩KTX2 / Basis Universal纹理体积减少 60-75%
合并网格Blender / glTF-Transform减少 draw calls
LOD 层级Blender / 手动远距离渲染简化模型
顶点去重glTF-Transform减少顶点数量

[最佳实践] 生产环境中,强烈建议使用 DRACO 压缩 + KTX2 纹理 的 glTF 格式。模型文件从几十 MB 可以压缩到几百 KB,加载速度提升显著。


D. 勘误与Bug修复记录

本附录记录正文各章节中发现的代码 bug、逻辑错误和内容不一致问题,按严重程度分类。 状态标记:✅ 已修复 | ⏳ 待修复

🔴 严重级别 (CRITICAL) — 会导致程序崩溃或输出完全错误

Bug #1:第 9 章 — FLOAT_VEC4 setter 调用了 uniform3nv ✅

位置9初级入门 --- 绘制多个物体:进一步封装绘制方法.md 约第 373 行

问题

javascript
FLOAT_VEC4: {
  value: 0x8B52,
  setter: function(location, v){
    gl.uniform3fv(location, v);  // ❌ 应为 uniform4fv
  }
},

修复gl.uniform3fvgl.uniform4fvFLOAT_VEC4 有 4 个分量,调用 uniform3fv 只上传 3 个,第 4 个分量(如 alpha 通道)丢失。


Bug #2:第 9 章 — FLOAT_VEC3 和 FLOAT_VEC4 之间缺少逗号 ✅

位置:同上文件,约第 372 行

问题FLOAT_VEC3 的闭合花括号后直接跟 FLOAT_VEC4,缺少逗号分隔符,导致 JavaScript 语法错误。

修复:在 FLOAT_VEC3 闭合花括号后添加逗号。


Bug #3:第 9 章 — createUniformSetter 参数数量不匹配 ✅

位置:约第 352 行(调用处)和第 479 行(定义处)

问题

javascript
// 调用处(2 个参数)
var setter = createUniformSetter(program, uniformInfo);

// 定义处(3 个参数)
function createUniformSetter(gl, program, uniformInfo) { ... }

调用时缺少第一个参数 gl,导致函数内部 gl 实际上是 program 对象,program 实际上是 uniformInfo 对象。

修复:在调用处添加 gl 参数:createUniformSetter(gl, program, uniformInfo)


Bug #4:第 9 章 — createUniformSetter 内部引用了未定义变量 ✅ location

位置:约第 479-497 行

问题:函数内正确获取了 uniformLocation,但闭包中引用了未定义的 location,运行时抛出 ReferenceError

修复:将所有 location 替换为 uniformLocation


Bug #5:第 9 章 — new list() 大小写错误 ✅

位置:约第 510 行

问题let modelList = new list(); — 类名定义为 List(大写 L),调用时写成了 list(小写 l)。

修复new list()new List()


Bug #6:第 20 章 — lookAt 矩阵索引覆盖 ✅(最著名 bug)

位置20中级进阶 --- 坐标系变换:世界空间变换到观察空间.md 约第 189-198 行

问题

javascript
// 第三列,z 轴基向量
target[8] = zAxis.x;
target[9] = zAxis.y;
target[10] = zAxis.z;
target[11] = 0;

// 第四列,坐标系原点位置 ❌ 索引 8-11 与上面重复!
target[8] = cameraPosition.x;
target[9] = cameraPosition.y;
target[10] = cameraPosition.z;
target[11] = 1;

第四列(平移列)应使用索引 12-15,但代码写成了 8-11,覆盖了 z 轴基向量。

修复

javascript
target[12] = cameraPosition.x;
target[13] = cameraPosition.y;
target[14] = cameraPosition.z;
target[15] = 1;

Bug #7:第 20 章 — 叉积参数顺序错误 ✅

位置:约第 148 行

问题:数学推导为 xAxis = zAxis × upDirection,代码实现为 Vector3.cross(upDirection, zAxis)。叉积反交换律:a × b = -(b × a),导致 x 轴方向反转。

修复Vector3.cross(zAxis, upDirection)


Bug #8:第 31 章 — MVP 公式顺序错误 ✅

位置31高级应用 --- 3D模型的拾取原理与实现.md 约第 48 行

问题:公式写为 MVP = P × M × V,正确顺序应为 MVP = P × V × M(投影 × 视图 × 模型)。

修复MVP = P × M × VMVP = P × V × M


🟡 高级别 (HIGH) — 会导致特定功能失效

Bug #9:第 9 章 — 变量名 ✅ uniformsSetters vs uniformSetters

位置:约第 342 行和第 353 行

问题:声明为 uniformsSetters(多一个 s),赋值时写成 uniformSetters,创建了两个不同变量。

修复:统一为 uniformSetters


Bug #10:第 20 章 — 平行向量检测逻辑错误 ✅

位置:约第 155 行

问题Math.abs(upDirection.z == 1) — 对布尔值取绝对值,应为 Math.abs(upDirection.z) === 1

修复Math.abs(upDirection.z === 1)Math.abs(upDirection.z) > 0.99(浮点容差)


Bug #11:第 31 章 — NDC 到屏幕坐标公式错误 ✅

位置:约第 80 行

问题Xp4 = (2 × Xp5) / (sWidth - 1),正确公式为 Xp4 = (2 × Xp5) / sWidth - 1

修复:修正公式推导。


Bug #12:第 31 章 — getProFromCVV 使用了未定义变量 ✅ aspect

位置:约第 163 行

问题:函数 getProFromCVV(cvv, near, viewRadians) 内部引用了 aspect,但该变量既不是参数也不是局部变量。

修复:添加 aspect 参数。


🟠 中级别 (MEDIUM) — 影响理解或特定场景

Bug #13:第 6 章 — 学习目标承诺了 ✅"绘制圆形"和"绘制环形"但正文未包含

位置6初级入门 --- 画个矩形:用基本图形构建平面.md 约第 17-18 行

修复建议:添加使用 TRIANGLE_FAN 绘制圆形、使用 TRIANGLE_STRIP 绘制环形的示例代码。


Bug #14:第 9 章 — Z 轴旋转使用了 Y 轴旋转值 ✅

位置:约第 710 行

问题object.rotateZ(object.rotation[1] + rand(0.2, 0.5))rotation[1] 是 Y 轴旋转值,应为 rotation[2]

修复rotation[1]rotation[2]


Bug #15:第 31 章 — 包围盒优化仅提及未实现 ✅

位置:约第 250-252 行

修复建议:补充射线-AABB 相交检测的实现代码。


🔵 低级别 (LOW) — 拼写错误或风格问题

Bug位置问题修复
#16第 6 章第 58 行VO 应为 V0(字母 O → 数字 0)VOV0
#17第 27 章文件名深入探究 与其他章节 深入研究 不一致重命名为 27深入研究 --- ...
#18第 31 章第 108 行LaTeX 公式中的分号 Wp2 = 1;移除分号
#19第 20 章第 97 行LaTeX 公式中使用了中文全角逗号替换为 ASCII 逗号
#20第 9/31 章使用 var 和原型类而非 ES6 class风格问题,不影响运行

修复统计

图表渲染中…
严重程度数量占比
🔴 CRITICAL840%
🟡 HIGH420%
🟠 MEDIUM315%
🔵 LOW525%
合计20100%

[建议] 第 9 章和第 20 章的 bug 最为集中和严重,建议优先修复。第 9 章的封装代码存在多个相互关联的 bug(参数不匹配、变量名错误、类型错误),建议整体重写该章的封装部分。