架构

本指南跟随一帧画面穿过 geometry3d 的各个包,说明每项职责位于何处以及原因。细节和推导见各包的页面。

层次

Luna-Flow/linear-algebra  (Vector[Double], Matrix[Double])
        │
      core        meshes, transforms, normals, visibility, Lambert
        │
      view        camera, projection, perspective-correct depth, lens model
        │
    frontend      scene → DrawList; shadow map; LumaBuffer; exposure; flow; timelines
        │
   ┌────┼──────────────┐
backend/tui   backend/canvas   backend/gsap
   │              │                │
  demo       demo_canvas       demo_gsap

每个包只导入位于其上方的包。core、view 和 frontend 对字符、颜色、DOM 和操作系统一无所知;各后端互不知晓;只有演示程序会接触时钟、环境变量、文件或页面元素。

一帧的旅程

  1. 模型。 frontend 把每个 SceneObject 的 Transform3 应用到其网格(core)上,得到世界空间中的顶点。
  2. 阴影。 它从方向光的视角把世界空间场景渲染到 128 × 128 的正交深度图中。
  3. 视图。 它用 Camera3 的视图变换(view)把顶点映射到相机空间:xx 向右,yy 向上,zz 向前。
  4. 投影。 它用 PerspectiveProjection 把顶点投影到视口中:xs=W/2+sx/zx_s = W/2 + s x/z,ys=H/2−sy/zy_s = H/2 - s y/z,并保留 zz 作为深度。
  5. 剔除与着色。 对每个朝向眼睛的面,它计算朗伯项,按阴影贴图判定为受光的面的比例对其衰减,并以该亮度输出两个 DrawTriangle。
  6. 后端。 后端把 DrawList 转换为输出:
    • backend/tui 按 terminal_y_scale 压缩 yy,为每个三角形选取一个字符表字符,并借助深度缓冲光栅化到 FrameBuffer 中;
    • backend/canvas 光栅化到前端的 LumaBuffer 中,并在画布上绘制明暗相同的像素段;
    • backend/gsap 把三角形由远到近排序并写出 SVG 多边形。
  7. 呈现。 演示程序打印这一帧、在浏览器中绘制它,或把它存入 .tui3d 序列。

为何以绘制列表为边界

三个后端需要的东西完全相同:每个三角形在屏幕上的位置、它有多远、它有多亮。此前的一切都是与设备无关的几何,在前端只计算一次;此后的一切都是设备层面的决定。frontend 设计讨论了其他方案。

遮挡是各后端唯一不同的决定。TUI 和 Canvas 后端使用相同的深度缓冲规则,结果精确。SVG 没有深度缓冲,因此 GSAP 后端使用画家排序,它对单个凸物体是精确的,其他情况下是近似的(GSAP 设计)。

跨包约定

  • 坐标。 世界空间和相机空间遵循 Direct3D 的左手约定:使用默认相机时,+x+x 向右,+y+y 向上,+z+z 远离观察者。屏幕 yy 向下增大。面的环绕方向使 (b−a)×(c−a)(b - a) \times (c - a) 指向外部。
  • 光照方向。 Light 存储从场景指向光源的单位向量。
  • 深度。 投影之后,深度处处都是相机空间的 zz;越小越近。空像素的深度为 103010^{30}。
  • 容差。 @core.DEPTH_EPSILON = 10−910^{-9} 在每个包中决定退化法向量、齐次除法、零面积三角形以及深度测试的平局。
  • 宽松的构造函数。 构造函数用默认值替换无效的尺寸和计数,而不是失败;库中没有任何函数返回 Result 或抛出错误。
  • 只读记录。 公开结构体是 pub struct:调用者可以读取每个字段,但只能通过构造函数创建值。

不在本仓库中的内容

这里没有针对近平面的裁剪、没有场景图、没有材质或纹理、没有平滑着色、没有资源加载、没有物理,也没有空间索引。仓库约定要求文档在这些功能实现之前不要描述它们。