ModelTranslator: 按 spike 结论改 UV 观察图为 PIL 两端架构
headless Blender export_layout(PNG) 走 GPUOffScreen 不可用;改为 Blender 抽 UV 几何→sidecar JSON→系统 Python(Pillow) 绘 PNG。 拆 Task 5(绘图 TDD)/6(抽几何)/7(渲染清理),README 加 Pillow 依赖。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
890d12fb09
commit
f0046ea64d
@@ -213,13 +213,9 @@ unwrap 后加 `minimize_stretch` 松弛、pack 前加 `average_islands_scale`;
|
||||
```python
|
||||
SEAM_ANGLE_SWEEP = (55.0, 45.0, 35.0) # 首选 66° 不过门时依次下探的 seam 角度
|
||||
STRETCH_ITERS = 30 # minimize_stretch 松弛迭代次数
|
||||
UV_PREVIEW_SIZE = 1024 # UV 观察图边长(px)
|
||||
UV_PREVIEW_OPACITY = 0.25 # UV 观察图填充不透明度
|
||||
UV_LAYOUT_ADDONS = ("io_mesh_uv_layout",
|
||||
"bl_ext.blender_org.io_mesh_uv_layout") # export_layout 依赖的 add-on 候选名
|
||||
```
|
||||
|
||||
(若 Task 1 探针确认了确切 add-on 名,把它放在元组首位。)
|
||||
(UV 观察图相关常量不放这里——headless 无法用 Blender 出 PNG,绘图移到系统 Python 侧的 `uv_preview.py`,见 Task 5。)
|
||||
|
||||
- [ ] **Step 2: 替换 `_unwrap_seam`**
|
||||
|
||||
@@ -369,72 +365,190 @@ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
|
||||
---
|
||||
|
||||
## Task 5: UV 线框观察图导出
|
||||
## Task 5: `uv_preview.py` — PIL 绘 UV 线框 PNG(TDD)
|
||||
|
||||
最终 UV 定下后用 `export_layout` 导出 `<名>_low_uv.png`;`main()` 接线并写入 MT_SUMMARY。
|
||||
新模块,纯函数 `render_uv_png`:拿 UV 多边形(归一化坐标)用 Pillow 画白底半透明填充+线框 PNG。在系统 Python(有 PIL 12.2.0)单测。
|
||||
|
||||
**Files:**
|
||||
- Modify: `Tools/ModelTranslator/bl_decimate.py`(新增 `_ensure_uv_layout_addon` / `_export_uv_layout`;`main()` 接线 + summary 字段)
|
||||
- Create: `Tools/ModelTranslator/uv_preview.py`
|
||||
- Test: `Tools/ModelTranslator/tests/test_uv_preview.py`
|
||||
|
||||
- [ ] **Step 1: 新增导出函数**
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
Create `Tools/ModelTranslator/tests/test_uv_preview.py`:
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import uv_preview as up
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class TestToPx(unittest.TestCase):
|
||||
def test_origin_bottom_left_maps_to_bottom_left_pixel(self):
|
||||
# UV(0,0) 原点在左下 -> 图像左下角(x=pad, y=size-pad)
|
||||
x, y = up._to_px(0.0, 0.0, size=100, pad=10)
|
||||
self.assertAlmostEqual(x, 10.0)
|
||||
self.assertAlmostEqual(y, 90.0)
|
||||
|
||||
def test_top_right(self):
|
||||
x, y = up._to_px(1.0, 1.0, size=100, pad=10)
|
||||
self.assertAlmostEqual(x, 90.0)
|
||||
self.assertAlmostEqual(y, 10.0)
|
||||
|
||||
|
||||
class TestRenderUvPng(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.mkdtemp()
|
||||
|
||||
def _out(self, name="uv.png"):
|
||||
return os.path.join(self.tmp, name)
|
||||
|
||||
def test_writes_png_of_requested_size(self):
|
||||
polys = [[[0.1, 0.1], [0.4, 0.1], [0.25, 0.4]],
|
||||
[[0.6, 0.6], [0.9, 0.6], [0.9, 0.9], [0.6, 0.9]]]
|
||||
out = up.render_uv_png(polys, self._out(), size=128)
|
||||
self.assertTrue(os.path.isfile(out))
|
||||
with Image.open(out) as im:
|
||||
self.assertEqual(im.size, (128, 128))
|
||||
self.assertEqual(im.format, "PNG")
|
||||
|
||||
def test_empty_polygons_still_writes_blank(self):
|
||||
out = up.render_uv_png([], self._out("blank.png"), size=64)
|
||||
self.assertTrue(os.path.isfile(out))
|
||||
with Image.open(out) as im:
|
||||
self.assertEqual(im.size, (64, 64))
|
||||
|
||||
def test_degenerate_polygon_skipped_no_crash(self):
|
||||
out = up.render_uv_png([[[0.5, 0.5]]], self._out("deg.png"), size=64)
|
||||
self.assertTrue(os.path.isfile(out))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_uv_preview -v
|
||||
```
|
||||
Expected: FAIL — `ModuleNotFoundError: No module named 'uv_preview'`
|
||||
|
||||
- [ ] **Step 3: 写实现**
|
||||
|
||||
Create `Tools/ModelTranslator/uv_preview.py`:
|
||||
|
||||
```python
|
||||
"""UV 线框观察图渲染:把 UV 多边形(归一化 0-1 坐标)画成 PNG 供人工查阅。
|
||||
在系统 Python(有 Pillow)中运行——Blender 自带 Python 无 PIL,故 UV 几何由
|
||||
bl_decimate 导出为 JSON,本模块负责绘制。"""
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def _to_px(u, v, size, pad):
|
||||
"""UV(0-1, 原点左下) -> 像素(原点左上),含边距。"""
|
||||
span = size - 2 * pad
|
||||
x = pad + u * span
|
||||
y = pad + (1.0 - v) * span # V 轴翻转:图像 y 向下
|
||||
return x, y
|
||||
|
||||
|
||||
def render_uv_png(polygons, out_png, size=1024, pad=8,
|
||||
line=(30, 30, 30, 255), fill=(80, 140, 220, 64)):
|
||||
"""polygons: [[[u,v], ...], ...] 每个多边形一组 UV 顶点。
|
||||
白底 + 半透明填充 + 深色线框;写 PNG 到 out_png,返回 out_png。"""
|
||||
base = Image.new("RGBA", (size, size), (255, 255, 255, 255))
|
||||
overlay = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(overlay, "RGBA")
|
||||
for poly in polygons:
|
||||
pts = [_to_px(u, v, size, pad) for (u, v) in poly]
|
||||
if len(pts) >= 3:
|
||||
draw.polygon(pts, fill=fill)
|
||||
for poly in polygons:
|
||||
pts = [_to_px(u, v, size, pad) for (u, v) in poly]
|
||||
if len(pts) >= 2:
|
||||
draw.line(pts + pts[:1], fill=line, width=1)
|
||||
Image.alpha_composite(base, overlay).convert("RGB").save(out_png, "PNG")
|
||||
return out_png
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行测试确认通过**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_uv_preview -v
|
||||
```
|
||||
Expected: PASS(5 tests OK)
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
cd "d:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/uv_preview.py Tools/ModelTranslator/tests/test_uv_preview.py && git commit -m "ModelTranslator: 新增 uv_preview.render_uv_png——PIL 绘 UV 线框 PNG
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: `bl_decimate.py` 抽取 UV 几何到 sidecar JSON
|
||||
|
||||
Blender 端提取每面 UV 多边形写入 `<名>_low_uv.polys.json`,summary 报告 `uv_preview`(目标 PNG 路径)与 `uv_polys`(sidecar 路径)。绘图由系统 Python 侧(Task 7)完成。
|
||||
|
||||
**Files:**
|
||||
- Modify: `Tools/ModelTranslator/bl_decimate.py`(新增 `_collect_uv_polygons`;`main()` 接线 + summary 字段)
|
||||
|
||||
- [ ] **Step 1: 新增几何抽取函数**
|
||||
|
||||
在 `bl_decimate.py` 的 `_do_unwrap` 之后、`main` 之前新增:
|
||||
|
||||
```python
|
||||
def _ensure_uv_layout_addon():
|
||||
"""启用 UV Layout 导出 add-on(export_layout 依赖,factory-startup 默认关闭)。
|
||||
返回是否有一个候选启用成功。"""
|
||||
import addon_utils
|
||||
for name in UV_LAYOUT_ADDONS:
|
||||
try:
|
||||
if addon_utils.enable(name, default_set=True) is not None:
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
def _export_uv_layout(obj, uv_png_path, warnings):
|
||||
"""导出 UV 线框观察图 PNG;任何失败记警告返回 False,不中断主流程。"""
|
||||
import bpy
|
||||
if not _ensure_uv_layout_addon():
|
||||
warnings.append("UV Layout add-on 不可用,跳过观察图导出")
|
||||
return False
|
||||
try:
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.uv.select_all(action='SELECT')
|
||||
bpy.ops.uv.export_layout(filepath=uv_png_path, mode='PNG',
|
||||
size=(UV_PREVIEW_SIZE, UV_PREVIEW_SIZE),
|
||||
opacity=UV_PREVIEW_OPACITY)
|
||||
return True
|
||||
except (RuntimeError, SystemError, TypeError) as e:
|
||||
warnings.append("UV 观察图导出失败(%s)" % e)
|
||||
return False
|
||||
finally:
|
||||
if obj.mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
def _collect_uv_polygons(obj):
|
||||
"""提取每个面的 UV 多边形(归一化坐标):[[[u,v], ...], ...],供外部 PIL 绘图。
|
||||
无 UV 层返回空列表。"""
|
||||
import bmesh
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
uv = bm.loops.layers.uv.active
|
||||
polys = []
|
||||
if uv is not None:
|
||||
for f in bm.faces:
|
||||
polys.append([[l[uv].uv.x, l[uv].uv.y] for l in f.loops])
|
||||
bm.free()
|
||||
return polys
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `main()` 接线**
|
||||
- [ ] **Step 2: `main()` 接线(抽取 + 写 sidecar)**
|
||||
|
||||
在 `bl_decimate.py:325` 的 `uv_info = _do_unwrap(obj, unwrap_mode, warnings)` 之后、`out_dir = ...` 之前插入:
|
||||
在 `main()` 中 `uv_info = _do_unwrap(obj, unwrap_mode, warnings)` 之后、`out_dir = ...` 之前插入:
|
||||
|
||||
```python
|
||||
uv_png = os.path.splitext(os.path.abspath(out_fbx))[0] + "_uv.png"
|
||||
uv_preview = uv_png if _export_uv_layout(obj, uv_png, warnings) else None
|
||||
uv_polys_json = os.path.splitext(os.path.abspath(out_fbx))[0] + "_uv.polys.json"
|
||||
uv_preview = None
|
||||
try:
|
||||
polys = _collect_uv_polygons(obj)
|
||||
os.makedirs(os.path.dirname(uv_polys_json), exist_ok=True)
|
||||
with open(uv_polys_json, "w", encoding="utf-8") as fp:
|
||||
json.dump(polys, fp)
|
||||
uv_preview = uv_png # 目标 PNG;实际绘制由 model_decimate(系统 Python + PIL)完成
|
||||
except Exception as e:
|
||||
warnings.append("UV 几何导出失败(%s),跳过观察图" % e)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: summary 增加字段**
|
||||
|
||||
将 `main()` 末尾 `print("MT_SUMMARY " + json.dumps({...}))` 的 dict(`bl_decimate.py:332-335`)改为包含 `uv_preview`:
|
||||
将 `main()` 末尾 `print("MT_SUMMARY " + json.dumps({...}))` 的 dict 改为包含 `uv_preview` 与 `uv_polys`:
|
||||
|
||||
```python
|
||||
print("MT_SUMMARY " + json.dumps(
|
||||
{"src": os.path.basename(src), "fbx": os.path.basename(out_fbx),
|
||||
"tris_before": orig, "tris_after": cur, "target": target,
|
||||
"uv": uv_info, "uv_preview": uv_preview, "warnings": warnings},
|
||||
"uv": uv_info,
|
||||
"uv_preview": uv_preview,
|
||||
"uv_polys": uv_polys_json if uv_preview else None,
|
||||
"warnings": warnings},
|
||||
ensure_ascii=False))
|
||||
```
|
||||
|
||||
@@ -446,84 +560,101 @@ cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -c "import ast; ast.pa
|
||||
```
|
||||
Expected: `OK`
|
||||
|
||||
- [ ] **Step 5: 端到端确认观察图生成**
|
||||
- [ ] **Step 5: 端到端确认 sidecar 生成**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/gargoyle.fbx --tris 1000 && ls -la src/gargoyle_low_uv.png
|
||||
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/gargoyle.fbx --tris 1000 && ls -la src/gargoyle_low_uv.polys.json && head -c 120 src/gargoyle_low_uv.polys.json
|
||||
```
|
||||
Expected: 命令成功;`src/gargoyle_low_uv.png` 存在且非空。用 Read 工具打开图片肉眼确认是 UV 线框(岛分布可读)。
|
||||
Expected: 命令成功;`src/gargoyle_low_uv.polys.json` 存在且是形如 `[[[...]]]` 的 JSON(此时 Task 7 尚未接线,sidecar 不会被删除,属正常)。
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
cd "d:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/bl_decimate.py && git commit -m "ModelTranslator: 导出 <名>_low_uv.png UV 线框观察图
|
||||
cd "d:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/bl_decimate.py && git commit -m "ModelTranslator: bl_decimate 抽取 UV 几何到 sidecar JSON
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: CLI 打印观察图路径
|
||||
## Task 7: `model_decimate.py` 渲染 PNG + 清理 + 打印
|
||||
|
||||
`model_decimate.py` 日志追加 UV 观察图路径行。
|
||||
系统 Python 侧读 sidecar JSON、用 `uv_preview.render_uv_png` 出 PNG、删 sidecar、打印路径。
|
||||
|
||||
**Files:**
|
||||
- Modify: `Tools/ModelTranslator/model_decimate.py:36`(`for w in s["warnings"]` 循环之前)
|
||||
- Modify: `Tools/ModelTranslator/model_decimate.py`(UV 指标打印后、warnings 循环前)
|
||||
|
||||
- [ ] **Step 1: 追加打印**
|
||||
- [ ] **Step 1: 接线渲染**
|
||||
|
||||
在 `model_decimate.py` 的 UV 指标打印(`:34-35`)之后、`for w in s["warnings"]:`(`:36`)之前插入:
|
||||
在 `model_decimate.py` 的 UV 指标打印(现有 `print(" UV: ...")` 两行)之后、`for w in s["warnings"]:` 之前插入:
|
||||
|
||||
```python
|
||||
if s.get("uv_preview"):
|
||||
if s.get("uv_polys"):
|
||||
try:
|
||||
import json as _json
|
||||
from uv_preview import render_uv_png
|
||||
with open(s["uv_polys"], encoding="utf-8") as fp:
|
||||
polys = _json.load(fp)
|
||||
render_uv_png(polys, s["uv_preview"])
|
||||
os.remove(s["uv_polys"])
|
||||
print(" UV 观察图: %s" % s["uv_preview"])
|
||||
except Exception as e:
|
||||
print(" [警告] UV 观察图渲染失败:%s" % e)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 端到端确认打印**
|
||||
- [ ] **Step 2: 端到端确认 PNG 生成、sidecar 清理、日志打印**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/gargoyle.fbx --tris 1000
|
||||
cd "d:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/gargoyle.fbx --tris 1000 && ls -la src/gargoyle_low_uv.png && ( test -f src/gargoyle_low_uv.polys.json && echo "SIDECAR REMAINS (BAD)" || echo "sidecar cleaned OK" )
|
||||
```
|
||||
Expected: 日志含 `UV 观察图: ...gargoyle_low_uv.png` 一行。
|
||||
Expected: 日志含 `UV 观察图: ...gargoyle_low_uv.png`;`gargoyle_low_uv.png` 存在且非空;打印 `sidecar cleaned OK`。用 Read 工具打开 PNG 肉眼确认是 UV 线框(岛分布可读)。
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
cd "d:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/model_decimate.py && git commit -m "ModelTranslator: model_decimate CLI 打印 UV 观察图路径
|
||||
cd "d:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/model_decimate.py && git commit -m "ModelTranslator: model_decimate 渲染 UV 观察图 PNG 并清理 sidecar
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: README 更新
|
||||
## Task 8: README 更新
|
||||
|
||||
补充展开增强与观察图说明,保持文档与行为一致。
|
||||
|
||||
**Files:**
|
||||
- Modify: `Tools/ModelTranslator/README.md:49`(model_decimate.py 说明段)
|
||||
- Modify: `Tools/ModelTranslator/README.md`(依赖段 + model_decimate.py 说明段)
|
||||
|
||||
- [ ] **Step 1: 更新 model_decimate.py 说明**
|
||||
|
||||
将 `README.md:49` 那条 `**model_decimate.py**:…` 说明中的展开描述更新为反映新行为,追加两点:
|
||||
将 `README.md` 那条 `**model_decimate.py**:…` 说明整体替换为反映新行为:
|
||||
|
||||
```markdown
|
||||
- **model_decimate.py**:多 mesh 自动 join;三角化后 Decimate(collapse) 减到 `--tris`(±3%,最多 2 轮修正);旧 UV 全删后重展——默认锐边 seam 整岛展开(SLIM 展开后 `minimize_stretch` 松弛 + `average_islands_scale` 纹素均衡),质量门不达标(翻转 >2% 或重叠 >8%)先按更低 seam 角度扫描(55/45/35°)取过门且岛数最少者抢救,全失败才回退 Smart UV Project,`--unwrap smart` 可直接选投影式展开;排布默认用 UVPackmaster 4 重排(只在最终选定 UV 上跑一次),UVPM4 不可用自动沿用内置 pack_islands 布局;输出 `<名>_low.fbx`(`-o` 改目录)与 `<名>_low_uv.png`(UV 线框观察图,便于人工查阅切分/排布/碎岛),日志报 UV 岛数/利用率与实际生效的 seam 角度
|
||||
- **model_decimate.py**:多 mesh 自动 join;三角化后 Decimate(collapse) 减到 `--tris`(±3%,最多 2 轮修正);旧 UV 全删后重展——默认锐边 seam 整岛展开(SLIM 展开后 `minimize_stretch` 松弛 + `average_islands_scale` 纹素均衡),质量门不达标(翻转 >2% 或重叠 >8%)先按更低 seam 角度扫描(55/45/35°)取过门且岛数最少者抢救,全失败才回退 Smart UV Project,`--unwrap smart` 可直接选投影式展开;排布默认用 UVPackmaster 4 重排(只在最终选定 UV 上跑一次),UVPM4 不可用自动沿用内置 pack_islands 布局;输出 `<名>_low.fbx`(`-o` 改目录)与 `<名>_low_uv.png`(UV 线框观察图,便于人工查阅切分/排布/碎岛——Blender 抽 UV 几何,系统 Python 用 Pillow 绘制,因 headless 无 GPU 无法用 Blender 直接出 PNG),日志报 UV 岛数/利用率与实际生效的 seam 角度
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
- [ ] **Step 2: 依赖段补 Pillow**
|
||||
|
||||
在 `README.md` 的「## 依赖」列表中追加一行(放在「系统 Python 3.x(仅标准库)」之后或替换其括注):
|
||||
|
||||
```markdown
|
||||
- 系统 Python 3.x(标准库 + Pillow——仅 `model_decimate.py` 绘 UV 观察图用;`pip install Pillow`)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
cd "d:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/README.md && git commit -m "ModelTranslator: README 补充展开增强与 UV 观察图说明
|
||||
cd "d:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/README.md && git commit -m "ModelTranslator: README 补充展开增强、UV 观察图与 Pillow 依赖说明
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: 最终验证
|
||||
## Task 9: 最终验证
|
||||
|
||||
- [ ] **Step 1: 全量单测**
|
||||
|
||||
@@ -547,9 +678,10 @@ Expected:命令成功;日志有 `模式`、UV 指标、`UV 观察图:` 行
|
||||
|
||||
---
|
||||
|
||||
## Self-Review 记录
|
||||
## Self-Review 记录(2026-07-21 spike 后修订)
|
||||
|
||||
- **Spec 覆盖**:①松弛+纹素均衡→Task 3;②角度扫描分级抢救→Task 2(纯函数)+Task 4;③UV 线框观察图→Task 5;CLI 打印→Task 6;错误处理表→各 Blender 任务的 try/except;实现首步验证项→Task 1 spike;README→Task 7;测试/验收→Task 2/4/5/8。全部有对应任务。
|
||||
- **Spec 覆盖**:①松弛+纹素均衡→Task 3;②角度扫描分级抢救→Task 2(纯函数)+Task 4;③UV 线框观察图→Task 5(PIL 绘图 TDD)+Task 6(Blender 抽几何)+Task 7(渲染/清理/打印);错误处理→各任务 try/except;实现首步验证→Task 1 spike(已完成,结论见下);README+依赖→Task 8;测试/验收→Task 2/4/5/7/9。全部有对应任务。
|
||||
- **Task 1 spike 结论**:`minimize_stretch` / `average_islands_scale` headless 可用;`export_layout(mode='PNG')` headless **不可用**(GPUOffScreen 后台无 GPU);`io_mesh_uv_layout` 是正确 add-on 名但仍受 GPU 限制。故观察图改为 Blender 抽 UV 几何→系统 Python(PIL) 绘 PNG 的两端架构(Task 5–7),不再用 `export_layout`。
|
||||
- **占位符**:无 TBD/TODO;每个代码步给出完整代码与确切命令、预期输出。
|
||||
- **类型/命名一致**:`pick_best_candidate`(Task 2 定义,Task 4 调用一致);`_unwrap_seam(obj, warnings, seam_angle_deg=...)`(Task 3 定义,Task 4 调用一致);`_export_uv_layout`/`_ensure_uv_layout_addon`(Task 5 定义与调用一致);`uv_preview` 字段(Task 5 产出,Task 6 消费,键名一致);常量 `SEAM_ANGLE_SWEEP`/`STRETCH_ITERS`/`UV_PREVIEW_SIZE`/`UV_PREVIEW_OPACITY`/`UV_LAYOUT_ADDONS`(Task 3 定义,Task 4/5 使用一致)。
|
||||
- **已知风险**:三个新算子/add-on 的 headless 可用性由 Task 1 先验;不可用则既有 try/except 降级,不中断。
|
||||
- **类型/命名一致**:`pick_best_candidate`(Task 2 定义,Task 4 调用一致);`_unwrap_seam(obj, warnings, seam_angle_deg=...)`(Task 3 定义,Task 4 调用一致);`render_uv_png`/`_to_px`(Task 5 定义,Task 7 调用一致);`_collect_uv_polygons`(Task 6 定义);summary 字段 `uv_preview`(PNG 路径)与 `uv_polys`(sidecar 路径)(Task 6 产出,Task 7 消费,键名一致);常量仅 `SEAM_ANGLE_SWEEP`/`STRETCH_ITERS`(Task 3 定义,Task 4 使用)——UV 预览尺寸/透明度作为 `render_uv_png` 默认参数,不再是 bl_decimate 常量。
|
||||
- **已知风险**:Blender 抽几何在无 UV 层时返回空列表(Task 6 已处理);渲染在系统 Python 侧,PIL 已确认可用(12.2.0);sidecar 清理失败仅影响残留文件,不影响 FBX/PNG 产出。
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
- 不抽独立「多候选择优」模块;改动内联进现有函数。
|
||||
- 观察图仅纯 UV 线框,不叠贴图/棋盘格(减面阶段尚无烘焙贴图)。
|
||||
- 不改 `model_bake.py` / `model_translator.py` / UVPM 排布逻辑。
|
||||
- 不引入 matplotlib/PIL 等新依赖。
|
||||
- ~~不引入 matplotlib/PIL 等新依赖。~~ **(2026-07-21 修订)** headless Blender 的 `export_layout(mode='PNG')` 走 GPUOffScreen,后台模式不可用(`SystemError: GPU functions ... not available in background mode`);`mode='SVG'` 可用但非位图。经用户确认,观察图改为**用系统 Python 的 Pillow 自绘 PNG 线框**——引入 Pillow 依赖(系统 Python 已装 12.2.0;Blender 自带 Python 无 PIL)。
|
||||
|
||||
## 设计
|
||||
|
||||
@@ -64,17 +64,16 @@ bpy.ops.uv.pack_islands(rotate=True, margin=PACK_MARGIN) # 已有
|
||||
|
||||
**质量门与常量**:`UV_FLIP_TOL`/`UV_OVERLAP_TOL`/`uv_gate_ok` 不变。新增 `SEAM_ANGLE_SWEEP`。
|
||||
|
||||
### 3. UV 线框观察图导出
|
||||
### 3. UV 线框观察图(Blender 抽几何 → 系统 Python 用 PIL 绘 PNG)
|
||||
|
||||
最终 UV 定下后(UVPM 重排完成后),导出线框 PNG:
|
||||
因 headless 无法用 Blender 直接出 PNG(见非目标修订),拆成两端:
|
||||
|
||||
- 算子:`bpy.ops.uv.export_layout(filepath=..., mode='PNG', size=(UV_PREVIEW_SIZE, UV_PREVIEW_SIZE), opacity=UV_PREVIEW_OPACITY)`。
|
||||
- 新增常量 `UV_PREVIEW_SIZE = 1024`、`UV_PREVIEW_OPACITY = 0.25`。
|
||||
- 需要正确上下文:对象激活、edit mode、UV 全选。实现时验证 headless 可行性。
|
||||
- 输出路径:紧邻 `_low.fbx`,命名 `<名>_low_uv.png`(由 `out_fbx` 派生:同目录、`os.path.splitext(out_fbx)[0] + "_uv.png"`)。
|
||||
- 失败:`try/except`(`RuntimeError`/`SystemError`)记警告、不中断,FBX 照常产出。
|
||||
- 封装为 `_export_uv_layout(obj, uv_png_path, warnings)`,在 `main()` 导出 FBX 前后择一时机调用(需在切回 OBJECT 模式前后处理好上下文)。
|
||||
- `MT_SUMMARY` JSON 增加字段 `uv_preview`(路径或 null)。
|
||||
**Blender 端(`bl_decimate.py`)**:最终 UV 定下后,`_collect_uv_polygons(obj)` 用 bmesh 提取每个面的 UV 多边形(归一化 0-1 坐标)`[[[u,v],...], ...]`,`main()` 写入 sidecar JSON `<名>_low_uv.polys.json`;`MT_SUMMARY` 增加字段 `uv_preview`(目标 PNG 路径 `<名>_low_uv.png`)与 `uv_polys`(sidecar JSON 路径);抽取失败记警告、两字段置 null、不中断。
|
||||
|
||||
**系统 Python 端(`uv_preview.py` + `model_decimate.py`)**:
|
||||
- 新模块 `uv_preview.py` 提供纯函数 `render_uv_png(polygons, out_png, size=1024, pad=8, ...)`:白底、半透明填充 + 深色线框(V 轴翻转到图像坐标),用 Pillow 绘制并存 PNG。可在系统 Python 单测。
|
||||
- `model_decimate.py` 拿到 summary 后:读 `uv_polys` JSON → 调 `render_uv_png` 输出到 `uv_preview` → 删除 sidecar JSON → 打印 PNG 路径;任一步失败打印警告、不中断(FBX 已产出)。
|
||||
- 输出:紧邻 `_low.fbx`,命名 `<名>_low_uv.png`。
|
||||
|
||||
### 4. CLI 输出(model_decimate.py)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user