Files
AIC-Project/docs/superpowers/specs/2026-07-23-adaptive-ray-bake-design.md
2026-07-23 17:25:44 +08:00

5.9 KiB
Raw Permalink Blame History

model_bake 自适应射线距离 设计

日期:2026-07-23

背景与动机

真机诊断确认:--reducer quad 低模烘焙时,前裙等薄部件出现深色"脏斑"。量化结果: supergirl quad 低模 7.06% 的面selected-to-active 烘焙射线穿透薄表面、打到背/内面 (命中法线与低模面法线反向),把里面的深色烘到正面。

  • 机制bl_bake._bake_pass 用固定 cage_extrusion=raymax_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.pycollapse/quad 通用)

纯函数(可单测,不 import bpy)

  • percentile(values, q) -> float:插值分位数;空列表或 q∉[0,1] 抛 ValueError
  • 常量 RAY_SAFETY=1.5RAY_LOWER_PCT=0.0005RAY_UPPER_PCT=0.01
  • adaptive_ray_distance(distances, bbox_dims) -> dict
    • diagonal = sqrt(sum(d^2))diagonal<=0ValueError
    • 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_passcage_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 覆盖)。