# model_bake 自适应射线距离 设计 日期:2026-07-23 ## 背景与动机 真机诊断确认:`--reducer quad` 低模烘焙时,前裙等**薄部件**出现深色"脏斑"。量化结果: supergirl quad 低模 **7.06% 的面**,selected-to-active 烘焙射线**穿透薄表面、打到背/内面** (命中法线与低模面法线反向),把里面的深色烘到正面。 - **机制**:`bl_bake._bake_pass` 用固定 `cage_extrusion=ray`、`max_ray_distance=ray*2`, 其中 `ray = 低模包围盒对角线 * 2%`(`DEFAULT_RAY_PCT`)。对薄部件,`max_ray`(=4% 对角线) 远大于裙子厚度 → 射线穿透。 - **为何 quad 特有**:QR 重拓扑低模偏离高模表面;collapse 用原始顶点贴在高模上,射线第一下 即中正面,故干净。 - **为何自适应能修**:7% 穿透面的面心其实离**正面**很近(分离≈0,只是射线过长冲到背面)。 按实测分离距离把射线调小后,射线先命中最近的正面 → 穿透面转为正面命中,脏斑消除。 固定 2% 射线是问题根源。改为**按实测低↔高分离距离**自适应,是 `2026-07-16-model-bake` 计划早已设计但未落地的 `adaptive_ray_distance` 思路,本 spec 聚焦落地这一块。 ## 约束 - 不加外部依赖(Python 标准库 + `mathutils`)。 - 显式 `--ray-distance` 仍优先、覆盖自适应。 - 自适应失败(无分离样本/包围盒退化)**降级回退**到现有固定 2%,绝不中断烘焙。 - 不改烘焙 pass 结构、输出命名、贴图内容协议;`MT_SUMMARY` 只增字段不改既有。 - 纯逻辑(percentile、adaptive_ray_distance)与 Blender 操作分离,纯逻辑 TDD。 - 不做 `2026-07-16` 计划的法线/UV 修复;不加 cage 物体支持(YAGNI)。 ## 架构(全在 `bl_bake.py`,collapse/quad 通用) ### 纯函数(可单测,不 import bpy) - `percentile(values, q) -> float`:插值分位数;空列表或 q∉[0,1] 抛 `ValueError`。 - 常量 `RAY_SAFETY=1.5`、`RAY_LOWER_PCT=0.0005`、`RAY_UPPER_PCT=0.01`。 - `adaptive_ray_distance(distances, bbox_dims) -> dict`: - `diagonal = sqrt(sum(d^2))`;`diagonal<=0` 抛 `ValueError`。 - `p99 = percentile(distances, 0.99)`;`raw = max(p99*RAY_SAFETY, diagonal*RAY_LOWER_PCT)`; `value = min(raw, diagonal*RAY_UPPER_PCT)`。 - 返回 `{"distance_p99": p99, "value": value, "capped": raw > diagonal*RAY_UPPER_PCT}`。 - 即:射线 = 分离 P99×1.5,夹在包围盒对角线 0.05%~1% 之间(旧固定值是 2%,故必然更小)。 ### Blender 侧 - `_measure_separation(low, high, limit=10000) -> list[float]`: - 高模建 `BVHTree.FromPolygons(高模世界坐标顶点, 多边形, all_triangles=False)`。 - 低模面若 ≤limit 全取,否则均匀采样 limit 个面;对每个面心(世界坐标)求 `bvh.find_nearest` 的距离,收集为距离数组(丢弃无命中的)。 - 返回距离列表(可能为空)。 ### main 集成 - `ray_arg == "auto"` 分支: - `dists = _measure_separation(low, high)`。 - 若 `dists` 非空:`info = adaptive_ray_distance(dists, tuple(low.dimensions))`;`ray = info["value"]`; `ray_source = "adaptive"`;`info["capped"]` 为真时记警告"自适应射线达对角线 1% 上限,检查高低模对应"。 - 若 `dists` 空 或 `adaptive_ray_distance` 抛异常:`ray = estimate_ray_distance(low_dims)`(回退固定 2%), 记警告"分离测量失败,回退固定射线",`ray_source = "fixed_fallback"`。 - `ray_arg != "auto"`:`ray = float(ray_arg)`,`ray_source = "explicit"`(不变)。 - `_bake_pass` 的 `cage_extrusion=ray, max_ray_distance=ray*2.0` **关系不变**(ray 已是自适应小值)。 ## 数据流 ``` import high/low --apply transform--> ray_arg=auto: high 建 BVH -> 低模面心最近距离数组 -> adaptive_ray_distance(P99*1.5, clamp[0.05%,1%]) (空/异常 -> 回退固定 2%) ray_arg=显式: float(ray_arg) --> 各 pass _bake_pass(cage=ray, max_ray=ray*2) --> PNG + MT_SUMMARY{ray_distance, ray:{source,p99,value,capped}} ``` ## MT_SUMMARY 变化 在现有 `"ray_distance": round(ray,6)` 基础上新增 `"ray"` 字段: `{"source": "adaptive|explicit|fixed_fallback", "distance_p99": , "value": , "capped": }`。 `model_bake.py` 打印一行射线信息(来源/P99/距离/是否夹取)。 ## 错误处理 | 情况 | 处理 | |---|---| | 低模无面 / BVH 无命中 → 分离数组空 | 回退固定 2% 射线,记警告 | | 低模包围盒对角线为 0(退化) | `adaptive_ray_distance` 抛 ValueError → 回退固定 2%,记警告 | | 显式 `--ray-distance` | 直接用,不测分离 | | 自适应值被夹到 1% 上限 | 记警告(提示高低模可能不匹配),仍用夹取值 | ## 测试 - **`tests/test_bl_bake.py`**(纯函数): - `percentile`:`[0,10]` q=0.25 → 2.5;空列表/q=1.1 抛 ValueError。 - `adaptive_ray_distance`: - 贴合样本 `[0.002]*20, bbox(1,0,0)` → value=0.003(P99×1.5,未夹)、capped=False。 - 全 0 分离 → value=下限 0.0005、capped=False(下限保护)。 - 大分离 `[0.02]`,bbox(1,0,0) → value=0.01(夹到 1% 上限)、capped=True。 - 零包围盒 → ValueError。 - **回归**:`python -m unittest discover -s tests` 全绿。 - **真机 smoke**: - 重烘 supergirl quad → 用 backhit 诊断(临时脚本)验证**穿透面 7% → 近 0**,前裙脏斑消失, 且无大面积落空(黑像素不显著增加)。 - collapse 回归:射线 source=adaptive、烘焙正常,贴图与升级前相当。 - 显式 `--ray-distance 0.02` → source=explicit,行为不变。 ## 明确不做(YAGNI) - 不做法线/UV 自动修复(`2026-07-16` 计划另一块)。 - 不加 cage 物体支持、不加 per-face 射线(单一自适应全局值已解决穿透)。 - 不新增 CLI 旋钮(复用现有 `--ray-distance` 覆盖)。