ModelTranslator: document adaptive ray bake

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ud18010
2026-07-23 17:25:44 +08:00
co-authored by Claude Opus 4.8
parent e66ba296c1
commit 6fe646310e
3 changed files with 506 additions and 1 deletions
@@ -0,0 +1,108 @@
# 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": <float|None>, "value": <float>, "capped": <bool>}`
`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.003P99×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` 覆盖)。