613 lines
24 KiB
Markdown
613 lines
24 KiB
Markdown
# RizomUV UV 展开接入实现计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** `model_decimate.py` 默认用 RizomUV(RizomUVLink 驱动)做 UV 自动切缝/展开/打包,不可用或质量门不达标自动回退 seam/smart。
|
||
|
||
**Architecture:** 三步管线——Blender 减面(`none` 模式不展 UV)→ 系统 Python 经官方 RizomUVLink 驱动 rizomuv.exe(AutoSeam→Cut→Unfold→Pack→Save 覆盖 FBX)→ Blender `bl_uvgate.py` 量 UV 指标做质量门,不达标就地 seam 重展。设计文档:`docs/superpowers/specs/2026-07-17-rizomuv-unwrap-design.md`。
|
||
|
||
**Tech Stack:** Python 3.12(系统)、RizomUVLink(RizomUV 2025 安装自带,MIT,`rizomuvlink_python312.pyd`)、Blender 5.0 headless。
|
||
|
||
**已核实的 API 事实**(来自 `C:\Program Files\RizomUV2025\RizomUVLink\RizomUVLinkBase.py` docstring 与 `doc/index.html`):
|
||
|
||
- `CRizomUVLink()` 方法:`RunRizomUV(exePath=...)`(启动实例并连接,返回端口)、`Load/Select/Cut/Unfold/Pack/Save/Quit(params)`;异常类型 `CZEx`
|
||
- 自动切缝 = `Select` 边工作集 + `Auto` 表:`Auto.QuasiDevelopable`(标准版叫 MOSAIC,`Developability` 0~1 为分割力度)、`Auto.HandleCutter`(切柄)、`Auto.PipesCutter`(连洞)、`Auto.SkeletonUnoverlap`(防重叠后处理)、`Auto.FlatteningMode`(0=unfold)、`Auto.Quality`
|
||
- `Cut`:`{"PrimType": "Edge", "WorkingSet": "Visible"}` 按选中边切缝
|
||
- `Unfold` 参数:`BorderIntersections`、`TriangleFlips`(bool,防边界交叉/翻转)、`Iterations`
|
||
- `Pack` 参数:顶层 `RootGroup`/`WorkingSet`/`ProcessTileSelection`/`Translate`/`RecursionDepth`,`Global` 表内 `PaddingSize`(岛间距,UV 单位)、`MarginSize`(tile 边距,UV 单位)、`MapResolution`(int)、`Rotate.Step`(度)、`Scaling.Mode`(int 枚举,顺序 0=OFF、1=SET_3D_AREA、2=AVG_RATIO_OF_MOVABLE_ISLANDS、3=AVG_RATIO_OF_GROUP;文档明言首次展开后建议 AVG_RATIO_OF_GROUP)
|
||
- 注册表查找(官方 README):`HKLM\SOFTWARE\Rizom Lab\RizomUV VS RS 202X.Y` 的默认值键 `rizomuv.exe`——**实测该值是不带后缀的安装前缀**(如 `C:\Program Files\RizomUV2025\rizomuv`),官方代码只取 `dirname` 用,须拼回 `rizomuv.exe`
|
||
- 本机安装:`C:\Program Files\RizomUV2025\rizomuv.exe`
|
||
|
||
**风险提示(执行者注意)**:`Select.Auto`/`Pack.Global` 的个别键名若被 RizomUV 拒绝(CZEx 报 unknown parameter),以 `link.Select.__doc__` / `link.Pack.__doc__` 打印出的参数名为准就地修正,不要猜。
|
||
|
||
---
|
||
|
||
### Task 1: bl_decimate.py——质量门纯函数 `uv_gate_ok` + `none` 展开模式
|
||
|
||
**Files:**
|
||
- Modify: `Tools/ModelTranslator/bl_decimate.py`(阈值常量附近 + `_do_unwrap` + `main`)
|
||
- Test: `Tools/ModelTranslator/tests/test_bl_decimate.py`
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
在 `tests/test_bl_decimate.py` 末尾(`if __name__` 之前,若无该块则文件末尾)追加:
|
||
|
||
```python
|
||
class TestUvGateOk(unittest.TestCase):
|
||
def test_clean_passes(self):
|
||
self.assertTrue(bd.uv_gate_ok(0.0, 0.0))
|
||
|
||
def test_boundary_passes(self):
|
||
self.assertTrue(bd.uv_gate_ok(bd.UV_FLIP_TOL, bd.UV_OVERLAP_TOL))
|
||
|
||
def test_flip_exceeds_fails(self):
|
||
self.assertFalse(bd.uv_gate_ok(0.03, 0.0))
|
||
|
||
def test_overlap_exceeds_fails(self):
|
||
self.assertFalse(bd.uv_gate_ok(0.0, 0.09))
|
||
|
||
def test_overlap_none_treated_as_zero(self):
|
||
self.assertTrue(bd.uv_gate_ok(0.01, None))
|
||
```
|
||
|
||
- [ ] **Step 2: 跑测试确认失败**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_bl_decimate -v 2>&1 | tail -15
|
||
```
|
||
|
||
预期:5 个新用例 ERROR(`AttributeError: module 'bl_decimate' has no attribute 'uv_gate_ok'`),旧用例全过。
|
||
|
||
- [ ] **Step 3: 实现 `uv_gate_ok` 并让 `_do_unwrap` 复用**
|
||
|
||
`bl_decimate.py` 中 `UV_OVERLAP_TOL` 常量定义之后加:
|
||
|
||
```python
|
||
def uv_gate_ok(flipped, overlap):
|
||
"""UV 质量门:翻转与重叠占比都在阈值内。overlap 为 None(op 不可用)按 0 处理。"""
|
||
return flipped <= UV_FLIP_TOL and (overlap or 0.0) <= UV_OVERLAP_TOL
|
||
```
|
||
|
||
`_do_unwrap` 中原判断:
|
||
|
||
```python
|
||
m = _collect_uv_metrics(obj)
|
||
overlap = m["overlap"] or 0.0
|
||
if m["flipped"] <= UV_FLIP_TOL and overlap <= UV_OVERLAP_TOL:
|
||
```
|
||
|
||
改为:
|
||
|
||
```python
|
||
m = _collect_uv_metrics(obj)
|
||
if uv_gate_ok(m["flipped"], m["overlap"]):
|
||
```
|
||
|
||
- [ ] **Step 4: 跑测试确认通过**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_bl_decimate -v 2>&1 | tail -5
|
||
```
|
||
|
||
预期:`OK`。
|
||
|
||
- [ ] **Step 5: 加 `none` 展开模式**
|
||
|
||
`bl_decimate.py` 的 `main()` 中,`uv_info = _do_unwrap(obj, unwrap_mode, warnings)` 一行改为:
|
||
|
||
```python
|
||
if unwrap_mode == "none":
|
||
# rizom 管线:跳过展开,UV 由后续 RizomUV 步骤生成
|
||
_clear_uv_layers(obj.data)
|
||
uv_info = {"mode": "none", "islands": 0, "fill": 0.0,
|
||
"flipped": 0.0, "overlap": None}
|
||
else:
|
||
uv_info = _do_unwrap(obj, unwrap_mode, warnings)
|
||
```
|
||
|
||
文件头 docstring 的调用说明 `[seam|smart]` 改为 `[seam|smart|none]`。
|
||
|
||
- [ ] **Step 6: 冒烟验证 none 模式**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && "D:/tools/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python bl_decimate.py -- src/store.fbx /tmp/store_none.fbx 3000 none 2>&1 | grep MT_SUMMARY
|
||
```
|
||
|
||
预期:`"uv": {"mode": "none", ...}`,无报错。
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/bl_decimate.py Tools/ModelTranslator/tests/test_bl_decimate.py && git commit -m "ModelTranslator: 质量门抽纯函数 uv_gate_ok + 减面 none 模式(rizom 管线前置)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: rizom_unwrap.py——exe 发现(纯逻辑 + 注册表)
|
||
|
||
**Files:**
|
||
- Create: `Tools/ModelTranslator/rizom_unwrap.py`
|
||
- Test: `Tools/ModelTranslator/tests/test_rizom_unwrap.py`
|
||
|
||
- [ ] **Step 1: 写失败测试**
|
||
|
||
新建 `tests/test_rizom_unwrap.py`:
|
||
|
||
```python
|
||
import os
|
||
import sys
|
||
import unittest
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
import rizom_unwrap as ru
|
||
|
||
|
||
class TestCandidatePaths(unittest.TestCase):
|
||
def test_priority_order(self):
|
||
self.assertEqual(ru.candidate_paths("a.exe", "b.exe", "c.exe"),
|
||
["a.exe", "b.exe", "c.exe", ru.DEFAULT_RIZOM])
|
||
|
||
def test_none_entries_skipped(self):
|
||
self.assertEqual(ru.candidate_paths(None, None, None), [ru.DEFAULT_RIZOM])
|
||
|
||
def test_env_only(self):
|
||
self.assertEqual(ru.candidate_paths(None, "b.exe", None),
|
||
["b.exe", ru.DEFAULT_RIZOM])
|
||
|
||
|
||
class TestFindRizomuv(unittest.TestCase):
|
||
def test_missing_exe_raises(self):
|
||
os.environ.pop("RIZOMUV_EXE", None)
|
||
with self.assertRaises(ru.RizomError):
|
||
ru.find_rizomuv("Z:/no/such/rizomuv.exe", use_registry=False,
|
||
default="Z:/no/default.exe")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|
||
```
|
||
|
||
- [ ] **Step 2: 跑测试确认失败**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_rizom_unwrap -v 2>&1 | tail -8
|
||
```
|
||
|
||
预期:`ModuleNotFoundError: No module named 'rizom_unwrap'`。
|
||
|
||
- [ ] **Step 3: 实现路径发现**
|
||
|
||
新建 `rizom_unwrap.py`:
|
||
|
||
```python
|
||
"""RizomUV 自动展开:官方 RizomUVLink 驱动 rizomuv.exe 做 AutoSeam/Unfold/Pack。
|
||
仅在系统 Python 中使用(不进 Blender);任何不可用/失败抛 RizomError,
|
||
由 model_decimate.py 回退 Blender seam 展开。"""
|
||
import os
|
||
import sys
|
||
import threading
|
||
|
||
DEFAULT_RIZOM = r"C:\Program Files\RizomUV2025\rizomuv.exe"
|
||
MAP_RES = 2048 # Pack 目标贴图分辨率
|
||
PADDING_PX = 4 # 岛间距(像素)= bl_decimate.PACK_MARGIN(0.002) * 2048
|
||
TIMEOUT_S = 300 # 单条命令超时
|
||
|
||
|
||
class RizomError(Exception):
|
||
"""RizomUV 不可用或处理失败(上层据此回退)。"""
|
||
|
||
|
||
def candidate_paths(cli_arg=None, env_val=None, reg_val=None, default=DEFAULT_RIZOM):
|
||
"""rizomuv.exe 候选路径,优先级:CLI 参数 > 环境变量 > 注册表 > 默认安装位置。"""
|
||
return [p for p in (cli_arg, env_val, reg_val, default) if p]
|
||
|
||
|
||
def registry_rizom_path():
|
||
"""官方 README 的注册表查找(HKLM\\SOFTWARE\\Rizom Lab\\RizomUV VS RS 202X.Y),
|
||
新版本优先;找不到或非 Windows 返回 None。"""
|
||
try:
|
||
import winreg
|
||
except ImportError:
|
||
return None
|
||
for i in range(9, 1, -1):
|
||
for j in range(10, -1, -1):
|
||
if i == 2 and j < 2:
|
||
continue
|
||
key_path = "SOFTWARE\\Rizom Lab\\RizomUV VS RS 202%d.%d" % (i, j)
|
||
try:
|
||
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path)
|
||
return winreg.QueryValue(key, "rizomuv.exe")
|
||
except OSError:
|
||
continue
|
||
return None
|
||
|
||
|
||
def find_rizomuv(cli_arg=None, use_registry=True, default=DEFAULT_RIZOM):
|
||
"""返回第一个真实存在的 rizomuv.exe;都不存在抛 RizomError。"""
|
||
reg = registry_rizom_path() if use_registry else None
|
||
cands = candidate_paths(cli_arg, os.environ.get("RIZOMUV_EXE"), reg, default)
|
||
for p in cands:
|
||
if os.path.isfile(p):
|
||
return p
|
||
raise RizomError("找不到 rizomuv.exe(尝试过:%s)" % "; ".join(cands))
|
||
```
|
||
|
||
- [ ] **Step 4: 跑测试确认通过**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_rizom_unwrap -v 2>&1 | tail -5
|
||
```
|
||
|
||
预期:`OK`(4 个用例)。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/rizom_unwrap.py Tools/ModelTranslator/tests/test_rizom_unwrap.py && git commit -m "ModelTranslator: rizom_unwrap exe 发现(CLI>env>注册表>默认,官方 README 口径)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: rizom_unwrap.py——RizomUVLink 展开驱动
|
||
|
||
**Files:**
|
||
- Modify: `Tools/ModelTranslator/rizom_unwrap.py`(文件末尾追加)
|
||
|
||
无法对真实 RizomUV 进程写单测;本任务以「对 store 低模跑通全流程」为验收(Step 3)。
|
||
|
||
- [ ] **Step 1: 实现驱动**
|
||
|
||
`rizom_unwrap.py` 末尾追加:
|
||
|
||
```python
|
||
def _import_link(exe):
|
||
"""从 RizomUV 安装目录动态引入官方 RizomUVLink 模块(随安装自带,MIT)。"""
|
||
link_dir = os.path.join(os.path.dirname(exe), "RizomUVLink")
|
||
if not os.path.isdir(link_dir):
|
||
raise RizomError("RizomUVLink 目录不存在:%s" % link_dir)
|
||
if link_dir not in sys.path:
|
||
sys.path.insert(0, link_dir)
|
||
try:
|
||
import RizomUVLink
|
||
return RizomUVLink
|
||
except Exception as e: # pyd 与 Python 版本不匹配等
|
||
raise RizomError("RizomUVLink 导入失败:%s" % e)
|
||
|
||
|
||
def _call(label, fn, params, timeout=TIMEOUT_S):
|
||
"""带超时执行一条 RizomUV 命令;超时或 CZEx 抛 RizomError。"""
|
||
out, err = [], []
|
||
|
||
def run():
|
||
try:
|
||
out.append(fn(params))
|
||
except Exception as e:
|
||
err.append(e)
|
||
|
||
t = threading.Thread(target=run, daemon=True)
|
||
t.start()
|
||
t.join(timeout)
|
||
if t.is_alive():
|
||
raise RizomError("%s 超时(%ds)" % (label, timeout))
|
||
if err:
|
||
raise RizomError("%s 失败:%s" % (label, err[0]))
|
||
return out[0] if out else None
|
||
|
||
|
||
def rizom_unwrap(fbx_path, exe, map_res=MAP_RES, padding_px=PADDING_PX):
|
||
"""AutoSeam 全自动切缝 -> Unfold -> Pack -> 覆盖保存 fbx_path。失败抛 RizomError。"""
|
||
mod = _import_link(exe)
|
||
link = mod.CRizomUVLink()
|
||
try:
|
||
link.RunRizomUV(exePath=exe)
|
||
except Exception as e:
|
||
raise RizomError("RizomUV 启动失败(license/环境):%s" % e)
|
||
fbx_abs = os.path.abspath(fbx_path)
|
||
pad_uv = float(padding_px) / map_res
|
||
try:
|
||
_call("Load", link.Load, {"File.Path": fbx_abs, "File.XYZ": True})
|
||
# AutoSeam:MOSAIC(QuasiDevelopable) + 切柄/连洞 + 防重叠后处理
|
||
_call("Select(AutoSeam)", link.Select, {
|
||
"PrimType": "Edge", "WorkingSet": "Visible",
|
||
"Select": True, "ResetBefore": True,
|
||
"Auto": {
|
||
"QuasiDevelopable": {"Developability": 0.5, "IslandPolyNBMin": 1,
|
||
"AreaMinRatio": 0.0, "FitCones": False,
|
||
"Straighten": True},
|
||
"HandleCutter": True,
|
||
"PipesCutter": True,
|
||
"SkeletonUnoverlap": {},
|
||
"FlatteningMode": 0,
|
||
"FlatteningUnfoldParams": {"BorderIntersections": True,
|
||
"TriangleFlips": True},
|
||
"StoreCoordsUVW": True,
|
||
"Quality": 0.5,
|
||
}})
|
||
_call("Cut", link.Cut, {"PrimType": "Edge", "WorkingSet": "Visible"})
|
||
_call("Unfold", link.Unfold, {"PrimType": "Edge", "WorkingSet": "Visible",
|
||
"BorderIntersections": True,
|
||
"TriangleFlips": True})
|
||
# Scaling.Mode=3 (AVG_RATIO_OF_GROUP):官方文档建议首次展开后用该模式统一密度
|
||
_call("Pack", link.Pack, {
|
||
"RootGroup": "RootGroup", "WorkingSet": "Visible",
|
||
"ProcessTileSelection": False, "RecursionDepth": 1,
|
||
"Translate": True,
|
||
"Global": {"MapResolution": map_res,
|
||
"PaddingSize": pad_uv, "MarginSize": pad_uv,
|
||
"Rotate": {"Step": 90.0},
|
||
"Scaling": {"Mode": 3}}})
|
||
_call("Save", link.Save, {"File.Path": fbx_abs})
|
||
finally:
|
||
try:
|
||
link.Quit({})
|
||
except Exception:
|
||
pass
|
||
```
|
||
|
||
- [ ] **Step 2: 确认既有单测未被破坏**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_rizom_unwrap tests.test_bl_decimate 2>&1 | tail -3
|
||
```
|
||
|
||
预期:`OK`。
|
||
|
||
- [ ] **Step 3: 真机冒烟(临时脚本,不入库)**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && cp src/store_low.fbx /tmp/rz_smoke.fbx && python -c "
|
||
from rizom_unwrap import find_rizomuv, rizom_unwrap
|
||
rizom_unwrap('/tmp/rz_smoke.fbx', find_rizomuv())
|
||
print('SMOKE OK')"
|
||
```
|
||
|
||
预期:RizomUV 窗口短暂出现后关闭,打印 `SMOKE OK`,`/tmp/rz_smoke.fbx` 被覆盖(mtime 更新)。
|
||
若某条命令 CZEx 报未知参数:打印对应 `link.<方法>.__doc__` 查正确键名后修正代码,禁止瞎猜重试。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/rizom_unwrap.py && git commit -m "ModelTranslator: RizomUVLink 驱动 AutoSeam/Unfold/Pack(带 300s 超时,失败抛 RizomError)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: bl_uvgate.py——UV 质量门(不达标就地 seam 重展)
|
||
|
||
**Files:**
|
||
- Create: `Tools/ModelTranslator/bl_uvgate.py`
|
||
|
||
纯函数(门限判断)已在 Task 1 覆盖单测;本脚本是 Blender 编排层,以 Step 2/3 冒烟为验收。
|
||
|
||
- [ ] **Step 1: 实现**
|
||
|
||
新建 `bl_uvgate.py`:
|
||
|
||
```python
|
||
"""Blender 内运行:校验低模 FBX 的 UV 质量门;无 UV 或不达标就地 seam 重展并覆盖导出。
|
||
调用:blender -b --factory-startup --python bl_uvgate.py -- <low.fbx> [mode_label]
|
||
mode_label 仅用于日志/回退警告中标注上游展开方式(默认 rizom)。"""
|
||
import os
|
||
import sys
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
|
||
def main():
|
||
import bpy
|
||
import json
|
||
from bl_decimate import (join_meshes, uv_gate_ok, _collect_uv_metrics,
|
||
_do_unwrap)
|
||
argv = sys.argv[sys.argv.index("--") + 1:]
|
||
fbx = argv[0]
|
||
mode_label = argv[1] if len(argv) > 1 else "rizom"
|
||
warnings = []
|
||
|
||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
bpy.ops.import_scene.fbx(filepath=fbx)
|
||
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
|
||
if not meshes:
|
||
print("MT_SUMMARY " + json.dumps({"error": "FBX 中没有 mesh"},
|
||
ensure_ascii=False))
|
||
return
|
||
obj = join_meshes(meshes)
|
||
|
||
if obj.data.uv_layers:
|
||
m = _collect_uv_metrics(obj)
|
||
if uv_gate_ok(m["flipped"], m["overlap"]):
|
||
m["mode"] = mode_label
|
||
print("MT_SUMMARY " + json.dumps(
|
||
{"uv": m, "reexported": False, "warnings": warnings},
|
||
ensure_ascii=False))
|
||
return
|
||
warnings.append("%s UV 质量不达标(翻转 %.1f%% 重叠 %s),回退 seam"
|
||
% (mode_label, m["flipped"] * 100,
|
||
"%.1f%%" % (m["overlap"] * 100)
|
||
if m["overlap"] is not None else "未知"))
|
||
else:
|
||
warnings.append("%s 输出无 UV,回退 seam" % mode_label)
|
||
|
||
m = _do_unwrap(obj, "seam", warnings) # 内部自带 smart 回退
|
||
if m["mode"] == "seam":
|
||
m["mode"] = "seam_fallback"
|
||
bpy.ops.object.select_all(action='DESELECT')
|
||
obj.select_set(True)
|
||
bpy.ops.export_scene.fbx(filepath=os.path.abspath(fbx), use_selection=True,
|
||
path_mode='STRIP', embed_textures=False)
|
||
print("MT_SUMMARY " + json.dumps(
|
||
{"uv": m, "reexported": True, "warnings": warnings},
|
||
ensure_ascii=False))
|
||
|
||
|
||
if __name__ == "__main__" and "--" in sys.argv:
|
||
main()
|
||
```
|
||
|
||
- [ ] **Step 2: 冒烟——达标路径**
|
||
|
||
用 Task 3 冒烟产物(RizomUV 已展开的 FBX):
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && "D:/tools/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python bl_uvgate.py -- /tmp/rz_smoke.fbx rizom 2>&1 | grep MT_SUMMARY
|
||
```
|
||
|
||
预期:`"mode": "rizom"`、`"reexported": false`,翻转/重叠数值在阈值内。
|
||
|
||
- [ ] **Step 3: 冒烟——回退路径(无 UV)**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && "D:/tools/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python bl_decimate.py -- src/store.fbx /tmp/gate_noneuv.fbx 3000 none 2>&1 | grep MT_SUMMARY && "D:/tools/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python bl_uvgate.py -- /tmp/gate_noneuv.fbx rizom 2>&1 | grep MT_SUMMARY
|
||
```
|
||
|
||
预期:第二条输出 `"mode": "seam_fallback"`(或 `smart_fallback`)、`"reexported": true`、警告含"输出无 UV,回退 seam"。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/bl_uvgate.py && git commit -m "ModelTranslator: bl_uvgate UV 质量门——rizom 产物校验,无 UV/不达标就地 seam 重展"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: model_decimate.py——rizom 默认编排
|
||
|
||
**Files:**
|
||
- Modify: `Tools/ModelTranslator/model_decimate.py`
|
||
|
||
- [ ] **Step 1: 实现编排**
|
||
|
||
`model_decimate.py` 全文改为:
|
||
|
||
```python
|
||
"""FBX 减面 CLI:减到指定三角面数并重展 UV(默认 RizomUV 自动展开),输出 <名>_low.fbx。
|
||
用法:python model_decimate.py src/well1500.fbx --tris 5000 [-o dir]
|
||
[--unwrap rizom|seam|smart] [--rizom exe] [--blender exe]"""
|
||
import argparse
|
||
import os
|
||
|
||
from mt_run import find_blender, run_blender_script
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description=__doc__)
|
||
ap.add_argument("input", help="FBX 文件")
|
||
ap.add_argument("--tris", type=int, required=True, help="目标三角面数")
|
||
ap.add_argument("-o", "--out", default=None, help="输出目录(默认源文件同目录)")
|
||
ap.add_argument("--unwrap", choices=("rizom", "seam", "smart"), default="rizom",
|
||
help="UV 展开:rizom=RizomUV 自动切缝(默认,不可用回退 seam),"
|
||
"seam=锐边接缝整岛展开,smart=Smart UV Project")
|
||
ap.add_argument("--rizom", default=None, help="rizomuv.exe 路径(默认自动发现)")
|
||
ap.add_argument("--blender", default=None)
|
||
args = ap.parse_args()
|
||
if args.tris <= 0:
|
||
ap.error("--tris 必须为正整数")
|
||
|
||
blender = find_blender(args.blender)
|
||
name = os.path.splitext(os.path.basename(args.input))[0]
|
||
outdir = args.out or os.path.dirname(os.path.abspath(args.input))
|
||
out_fbx = os.path.join(outdir, name + "_low.fbx")
|
||
|
||
if args.unwrap == "rizom":
|
||
# 三步:Blender 减面(不展UV)-> RizomUV 展开 -> Blender 质量门(不达标就地 seam 重展)
|
||
s = run_blender_script(blender, "bl_decimate.py",
|
||
[args.input, out_fbx, str(args.tris), "none"])
|
||
warnings = list(s["warnings"])
|
||
from rizom_unwrap import RizomError, find_rizomuv, rizom_unwrap
|
||
try:
|
||
rizom_unwrap(out_fbx, find_rizomuv(args.rizom))
|
||
except RizomError as e:
|
||
warnings.append("RizomUV 不可用:%s" % e)
|
||
g = run_blender_script(blender, "bl_uvgate.py", [out_fbx, "rizom"])
|
||
s["uv"] = g["uv"]
|
||
s["warnings"] = warnings + g["warnings"]
|
||
else:
|
||
s = run_blender_script(blender, "bl_decimate.py",
|
||
[args.input, out_fbx, str(args.tris), args.unwrap])
|
||
|
||
print("== %s: %d -> %d 面(目标 %d)-> %s" %
|
||
(s["src"], s["tris_before"], s["tris_after"], s["target"], out_fbx))
|
||
u = s["uv"]
|
||
overlap = "%.1f%%" % (u["overlap"] * 100) if u["overlap"] is not None else "n/a"
|
||
print(" UV: 模式=%s 岛数=%d 利用率=%.1f%% 翻转=%.1f%% 重叠=%s" %
|
||
(u["mode"], u["islands"], u["fill"] * 100, u["flipped"] * 100, overlap))
|
||
for w in s["warnings"]:
|
||
print(" [警告] %s" % w)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
```
|
||
|
||
- [ ] **Step 2: 全量单测**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest discover -s tests -p "test_*.py" 2>&1 | tail -3
|
||
```
|
||
|
||
预期:`OK`。
|
||
|
||
- [ ] **Step 3: 端到端冒烟——rizom 默认路径**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/store10k.fbx --tris 10000
|
||
```
|
||
|
||
预期:`UV: 模式=rizom ...`,无"回退"警告;记录岛数/利用率/翻转/重叠,与旧 smart_fallback 基线(岛 1405 / 利用率 36.4%)对比,利用率应明显更高。
|
||
|
||
- [ ] **Step 4: 端到端冒烟——回退路径**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/store.fbx --tris 3000 --rizom Z:/no/such.exe
|
||
```
|
||
|
||
预期:警告含"RizomUV 不可用",`UV: 模式=seam_fallback ...` 且指标达标(store 3000 面 seam 基线:岛 388 / 利用率 54.9% / 翻转 0.2% / 重叠 6.2%)。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/model_decimate.py && git commit -m "ModelTranslator: 减面默认 RizomUV 展开(三步编排,失败/不达标回退 seam)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: 烘焙链路验证 + README
|
||
|
||
**Files:**
|
||
- Modify: `Tools/ModelTranslator/README.md`(依赖节 + model_decimate 说明)
|
||
|
||
- [ ] **Step 1: rizom 产物烘焙冒烟**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_bake.py src/store10k.fbx src/store10k_low.fbx
|
||
```
|
||
|
||
预期:五张贴图全出、无警告。用 Read 工具查看 `out_bake/store10k/store10k_color.png`:岛内无黑斑/脏色。
|
||
|
||
- [ ] **Step 2: 更新 README**
|
||
|
||
`README.md` 「依赖」节 Blender 条目后加:
|
||
|
||
```markdown
|
||
- RizomUV 2025+(可选,UV 展开质量更好;默认路径 `C:\Program Files\RizomUV2025\rizomuv.exe`,也可用 `--rizom` 参数或环境变量 `RIZOMUV_EXE` 指定;未安装自动回退 Blender 展开)
|
||
```
|
||
|
||
「减面与高低模烘焙」节中 model_decimate.py 的说明整句改为:
|
||
|
||
```markdown
|
||
- **model_decimate.py**:多 mesh 自动 join;三角化后 Decimate(collapse) 减到 `--tris`(±3%,最多 2 轮修正);旧 UV 全删后重展——默认 **RizomUV 自动切缝展开**(官方 RizomUVLink 驱动:MOSAIC 分割 + 切柄/连洞 + Pack,会短暂弹出 RizomUV 窗口、占一个 license),RizomUV 不可用或质量门不达标(翻转 >2% 或重叠 >8%)自动回退锐边 seam 展开(再不达标回退 Smart UV Project);`--unwrap seam|smart` 可跳过 RizomUV;输出 `<名>_low.fbx`(`-o` 改目录),日志报 UV 岛数/利用率
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/README.md && git commit -m "ModelTranslator: README 补 RizomUV 展开说明(store10k 冒烟对比通过)"
|
||
```
|
||
|
||
---
|
||
|
||
## 自审记录
|
||
|
||
- Spec 覆盖:默认 rizom(Task 5)、AutoSeam(Task 3)、回退链 rizom→seam→smart(Task 4/5)、exe 发现四级(Task 2)、超时(Task 3 `_call`)、无 UV 视同不达标(Task 4)、日志口径(Task 5 沿用)、README(Task 6)、单测+冒烟+回退冒烟(Task 1/2/5/6)——全覆盖
|
||
- 占位符扫描:无 TBD/省略;所有代码块完整
|
||
- 类型一致性:`uv_gate_ok(flipped, overlap)`(Task 1 定义,Task 4 使用)、`RizomError/find_rizomuv/rizom_unwrap`(Task 2/3 定义,Task 5 导入)、`candidate_paths(cli, env, reg, default)` 签名测试与实现一致
|