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