5.9 KiB
5.9 KiB
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.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覆盖)。