{
  "lead": "光栅化只画「光直接照到哪里」，路径追踪则模拟光在场景里的完整旅程：从相机出发的光线打到墙上，随机弹向下一个方向，再打到别的物体……直到碰到光源或者逃逸到天空。每个像素的颜色就是大量随机路径的平均值（蒙特卡洛积分）。路径越多越准确：刚开始满屏噪点，随着采样累积，柔和阴影、玻璃折射、墙面互相「染色」这些效果会逐渐清晰地浮现。",
  "formulas": [
    [
      "L = L<sub>e</sub> + ∫ f · L<sub>i</sub> · cos θ dω",
      "渲染方程：看到的光 = 自身发光 + 从各个方向照进来并被反射的光"
    ],
    [
      "像素颜色 ≈ (1/N) Σ 第 k 条路径带回的光",
      "蒙特卡洛估计：误差按 1/√N 减小，采样数翻 4 倍噪点减半"
    ]
  ],
  "terms": [
    [
      "漫反射与余弦采样",
      "粗糙表面把光均匀地散向各个方向，越接近法线方向贡献越大。按 cos θ 的概率分布挑选反弹方向，正好抵消掉公式里的 cos θ，同样的采样数噪点更少。"
    ],
    [
      "直接光采样（NEE）",
      "天花板上的灯很小，随机反弹的光线很少恰好打中它，画面会一直很噪。所以每次打到粗糙表面时，额外朝灯上随机一点发一条「阴影光线」：没被挡住就直接加上灯的贡献。为了不重复计算，漫反射之后再撞到灯时就不再计入它的发光。"
    ],
    [
      "金属与玻璃",
      "金属按反射定律镜面反射，加一点随机扰动就成了磨砂金属。玻璃按斯涅尔定律折射，再用 Schlick 近似算出反射比例，随机决定这条光线是反射还是折射；从玻璃内部以大角度射出时发生全反射。"
    ],
    [
      "俄罗斯轮盘赌",
      "路径每反弹一次，携带的能量就被表面颜色吸收一部分。反弹 3 次以后，按剩余能量的大小随机决定是否继续：终止的路径直接丢弃，继续的路径能量相应放大，这样既节省计算，平均结果又没有偏差。"
    ],
    [
      "渐进式累积",
      "每一「遍」给每个像素追踪一条新路径，加到累积缓冲区里，显示的是平均值。视角一变，之前的采样就作废，从头开始累积。偶尔出现的极亮路径（萤火虫噪点，多来自经玻璃聚焦的焦散）会被限制亮度，代价是焦散略暗一点。"
    ],
    [
      "多线程渲染",
      "每一行的随机数种子只由（行号，第几遍）决定，各行又写在缓冲区里互不重叠的位置，所以可以直接交给 rayon 线程池并行计算，得到的图像和单线程逐位相同。浏览器里的 WASM 线程就是多个 Web Worker 共享同一块 <code>SharedArrayBuffer</code> 内存，这要求页面「跨源隔离」（服务器返回 COOP/COEP 响应头），标准库也要用 nightly 工具链带着 atomics 特性重新编译，所以多线程版是单独构建的 <code>pkg/pathtracer-mt</code>。"
    ]
  ],
  "flow": [
    [
      "js",
      "页面启动一个 Web Worker，由它加载 WASM：满足条件时加载多线程版并调用 <code>initThreadPool(线程数)</code>，否则加载单线程版。rayon 在等待各线程时会阻塞调用它的线程，而浏览器禁止阻塞主线程，所以渲染必须放在 Worker 里——单线程版也这样做，页面因此始终流畅。"
    ],
    [
      "js",
      "主线程只负责交互：拖动、缩放时发送 <code>pt_set_camera</code>，并始终保持一个渲染请求在途，每批遍数按上一批的耗时调整到约 30ms。每次视角变化都带一个编号，旧视角渲染出的帧到达后直接丢弃。"
    ],
    [
      "rust",
      "Rust 逐行追踪路径（多线程版把各行分给线程池），累加进缓冲区；<code>pt_frame()</code> 把平均值做 gamma 校正后返回 RGBA 字节，Worker 把它以可转移对象发回主线程，主线程用 <code>putImageData</code> 显示。累积到 2048 采样后自动停止。"
    ]
  ],
  "perf": [
    "实测（桌面 Chromium，单线程）：康奈尔盒子 320×240 每遍约 32ms（约 240 万条路径/秒）；天空场景光线很快逃逸到天空，每遍约 7ms（约 1000 万条路径/秒）。房间是封闭的，光要反弹很多次才被吸收，所以比天空场景慢得多。",
    "路径追踪的计算量和像素数成正比：640×480 的像素是 320×240 的 4 倍，每遍耗时也约为 4 倍。",
    "实测多线程（康奈尔盒子 320×240，20 个逻辑核心的桌面 CPU）：单线程每遍 32ms，2 线程 18ms（1.8 倍），4 线程 9.1ms（3.5 倍），16 线程 3.4ms（约 9.5 倍）；多线程渲染出的图像与单线程逐字节相同。线程越多，各线程等待最慢那几行、以及每遍同步的开销占比越大，所以加速比低于线程数。",
    "多线程版需要跨源隔离：本项目的 <code>scripts/serve.py</code> 已返回 COOP/COEP 响应头；静态托管（如 Gitee Pages）通常不能设置响应头，页面会自动退回单线程。「线程」下拉框里不可用的选项会注明原因。",
    "康奈尔盒子的正面在经典场景里是敞开的，这里把房间向相机方向延长并封闭，这样转动视角时不会看到盒子外面的虚空；房间里相机原地转向，天空场景里相机绕着球旋转。"
  ],
  "src": [
    [
      "crates/graphics/pathtracer/src/lib.rs",
      ""
    ],
    [
      "crates/graphics/pathtracer/src/scene.rs",
      ""
    ],
    [
      "www/pathtracer/worker.js",
      ""
    ],
    [
      "www/pathtracer/index.js",
      ""
    ]
  ]
}
