diff --git a/Tools/ModelTranslator/bl_bake.py b/Tools/ModelTranslator/bl_bake.py index abdc19b2..f857c355 100644 --- a/Tools/ModelTranslator/bl_bake.py +++ b/Tools/ModelTranslator/bl_bake.py @@ -9,6 +9,9 @@ import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) DEFAULT_RAY_PCT = 0.02 # 自动射线距离 = 低模包围盒对角线 * 2% +RAY_SAFETY = 1.5 # 自适应射线安全系数(分离 P99 × 此值) +RAY_LOWER_PCT = 0.0005 # 射线下限 = 包围盒对角线 * 0.05% +RAY_UPPER_PCT = 0.01 # 射线上限 = 包围盒对角线 * 1% BBOX_TOL = 0.10 # 高低模包围盒尺寸相对差警告阈值 # (输出名, bake type, EMIT 来源的 Principled 输入, 目标图颜色空间) @@ -26,6 +29,34 @@ def estimate_ray_distance(bbox_dims, pct=DEFAULT_RAY_PCT): return math.sqrt(sum(d * d for d in bbox_dims)) * pct +def percentile(values, q): + """插值分位数。空列表或 q∉[0,1] 抛 ValueError。""" + if not values: + raise ValueError("percentile 需要至少一个值") + if not 0.0 <= q <= 1.0: + raise ValueError("分位数 q 必须在 [0,1]") + ordered = sorted(float(v) for v in values) + pos = (len(ordered) - 1) * q + lo = int(math.floor(pos)) + hi = int(math.ceil(pos)) + if lo == hi: + return ordered[lo] + return ordered[lo] + (ordered[hi] - ordered[lo]) * (pos - lo) + + +def adaptive_ray_distance(distances, bbox_dims): + """按低↔高分离距离定射线:P99×RAY_SAFETY,夹在对角线 [0.05%,1%]。 + 返回 {distance_p99, value, capped};包围盒退化抛 ValueError。""" + diagonal = math.sqrt(sum(float(d) * float(d) for d in bbox_dims)) + if diagonal <= 0.0: + raise ValueError("低模包围盒对角线必须为正") + p99 = percentile(distances, 0.99) + lower = diagonal * RAY_LOWER_PCT + upper = diagonal * RAY_UPPER_PCT + raw = max(p99 * RAY_SAFETY, lower) + return {"distance_p99": p99, "value": min(raw, upper), "capped": raw > upper} + + def output_stem(low_name): """低模名去掉 _low 后缀作为输出前缀(对齐工具3纯网格贴图命名约定)。""" return low_name[:-4] if low_name.endswith("_low") else low_name diff --git a/Tools/ModelTranslator/tests/test_bl_bake.py b/Tools/ModelTranslator/tests/test_bl_bake.py index 7351b529..6348de2f 100644 --- a/Tools/ModelTranslator/tests/test_bl_bake.py +++ b/Tools/ModelTranslator/tests/test_bl_bake.py @@ -43,5 +43,40 @@ class TestBboxMismatch(unittest.TestCase): self.assertFalse(bb.bbox_mismatch((1.0, 0.0, 3.0), (1.0, 0.0, 3.0))) +class TestPercentile(unittest.TestCase): + def test_interpolates(self): + self.assertAlmostEqual(bb.percentile([0.0, 10.0], 0.25), 2.5) + + def test_rejects_empty(self): + with self.assertRaises(ValueError): + bb.percentile([], 0.99) + + def test_rejects_bad_q(self): + with self.assertRaises(ValueError): + bb.percentile([1.0], 1.1) + + +class TestAdaptiveRayDistance(unittest.TestCase): + def test_p99_with_safety(self): + r = bb.adaptive_ray_distance([0.002] * 20, (1.0, 0.0, 0.0)) + self.assertAlmostEqual(r["distance_p99"], 0.002) + self.assertAlmostEqual(r["value"], 0.003) # 0.002*1.5,未夹 + self.assertFalse(r["capped"]) + + def test_lower_floor(self): + r = bb.adaptive_ray_distance([0.0], (1.0, 0.0, 0.0)) + self.assertAlmostEqual(r["value"], 0.0005) # 下限 0.05% + self.assertFalse(r["capped"]) + + def test_upper_cap(self): + r = bb.adaptive_ray_distance([0.02], (1.0, 0.0, 0.0)) + self.assertAlmostEqual(r["value"], 0.01) # 夹到 1% 上限 + self.assertTrue(r["capped"]) + + def test_zero_bbox_raises(self): + with self.assertRaises(ValueError): + bb.adaptive_ray_distance([0.01], (0.0, 0.0, 0.0)) + + if __name__ == "__main__": unittest.main()