{
  "lead": "显卡画 3D 画面的核心流程叫「光栅化管线」：把模型的每个顶点变换到屏幕上，扔掉背对相机的三角形，找出每个三角形覆盖了哪些像素，再用深度缓冲决定谁挡住谁、用光照公式给像素上色。这个演示不调用 WebGL，把这条管线的每一步都用 Rust 在 CPU 上实现一遍，WASM 算出整幅图像后交给 Canvas 显示。",
  "formulas": [
    [
      "p<sub>clip</sub> = P · V · M · p",
      "模型矩阵 M 旋转物体，视图矩阵 V 摆放相机，投影矩阵 P 产生近大远小"
    ],
    [
      "E(p) = (b − a) × (p − a)",
      "边函数：像素中心在三条边的同一侧，就在三角形内"
    ]
  ],
  "terms": [
    [
      "透视除法与视口变换",
      "投影后的坐标除以 w 得到 −1~1 的标准化坐标（NDC），再映射到像素坐标。w 越大（越远），除完越靠近画面中心，这就是透视。"
    ],
    [
      "背面剔除",
      "模型的三角形统一按逆时针绕向朝外。投影到屏幕后，如果三个顶点变成反方向，说明这一面背对相机，直接跳过。封闭模型通常能省掉一半以上的三角形；相机越近，能看到的部分越少，剔除得越多。"
    ],
    [
      "边函数与左上规则",
      "对三角形包围盒里的每个像素中心，计算它相对三条边的边函数。三个值同号就在三角形内，三个值除以总面积就是重心坐标。像素恰好落在边上时按「左上规则」只归一个三角形，所以相邻三角形的公共边既不会漏画，也不会重复画。"
    ],
    [
      "深度缓冲（z-buffer）",
      "每个像素记住目前画过的最近深度。新像素比它更近才写入颜色并更新深度，否则丢弃。这样三角形可以按任意顺序画，遮挡关系依然正确。「深度缓冲」模式把这张表直接画成灰度图：越近越亮。"
    ],
    [
      "透视校正插值",
      "屏幕上线性变化的是 1/w 而不是原始属性。插值法线等属性时先插值「属性/w」和「1/w」，再相除，否则贴近相机的大三角形上会出现扭曲。"
    ],
    [
      "平面着色与 Phong 着色",
      "平面着色每个三角形用自己的面法线，能看出一个个小平面；Phong 着色在每个像素插值顶点法线，再用 Blinn-Phong 公式计算环境光、漫反射和高光，曲面看起来就是光滑的。立方体的棱角是真实的，所以两种模式下都用面法线。"
    ]
  ],
  "flow": [
    [
      "js",
      "每一帧 <code>requestAnimationFrame</code> 更新旋转角，调用 <code>raster_render(宽, 高, 模型, rx, ry, 缩放, 模式)</code>。"
    ],
    [
      "rust",
      "Rust 生成（并缓存）模型网格，依次做顶点变换、背面剔除、光栅化、深度测试和着色，返回 RGBA 像素数组；<code>raster_stats()</code> 返回三角形总数、剔除数、绘制数和着色像素数。"
    ],
    [
      "js",
      "JS 把像素包装成 <code>ImageData</code>，用 <code>putImageData</code> 画到 Canvas 上，并在底部显示耗时和统计。"
    ]
  ],
  "perf": [
    "GPU 用成千上万个核心并行处理像素，这里只有一个 CPU 线程逐个像素计算，所以同样的画面要慢很多。好在 480×360 的分辨率和几千个三角形对 WASM 来说不算多，仍能流畅地实时旋转。",
    "像素数决定大部分开销：放大模型后覆盖的像素变多，每帧耗时随之上升；三叶结的三角形最多，但每个都很小，耗时反而和圆环差不多。",
    "这里省略了真正管线里的「裁剪」步骤：顶点跑到相机后面的三角形会被整个丢弃，所以缩放范围做了限制。"
  ],
  "src": [
    [
      "crates/graphics/rasterizer/src/lib.rs",
      ""
    ],
    [
      "www/rasterizer/index.js",
      ""
    ]
  ]
}
