根因(真机排查):QuadRemesher 引擎在激进减面下非确定性地从封闭流形 输入打出孔洞/非流形边(实测同一输入约 85% 次数破碎),即"破碎面"。 修复:QR 导回后比对输入拓扑,若比输入新增开边(孔洞)/非流形边即判破损、 弃用 QR 回退 collapse(topology_acceptable 纯策略+单测;_edge_defects 计数)。 顺带修复水密门在导入后拒绝时未恢复 obj 选中态,导致 collapse 回退时 export use_selection 选不到对象、只导出 4KB 空 FBX 的 bug。 含用户改动:--reducer 默认改为 quad(配合水密门,破碎自动回退,安全), 及其 CLI 默认值测试。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 lines
2.8 KiB
Python
73 lines
2.8 KiB
Python
"""QR 桥纯逻辑:引擎发现、面数换算、settings 生成、progress 解析。不 import bpy。
|
||
减面可选后端 Quad Remesher 用——headless 直接调 Engine/xremesh.exe,绕开 modal 操作符。"""
|
||
import glob
|
||
import os
|
||
|
||
QR_TIMEOUT = 120.0 # 引擎轮询超时(秒)
|
||
|
||
|
||
def find_qr_engine():
|
||
"""定位 QuadRemesher 的 xremesh.exe;找不到返回 None。
|
||
顺序:QR_ENGINE 环境变量(权威:显式设了就以它为准,无效即 None,不再自动发现)
|
||
-> 标准 addon 路径(取字典序最大目录,当前 Blender 5.x 下即最高版本)。"""
|
||
env = os.environ.get("QR_ENGINE")
|
||
if env:
|
||
return env if os.path.isfile(env) else None
|
||
appdata = os.environ.get("APPDATA")
|
||
if not appdata:
|
||
return None
|
||
pattern = os.path.join(
|
||
appdata, "Blender Foundation", "Blender", "*",
|
||
"scripts", "addons", "QuadRemesher", "Engine", "xremesh.exe")
|
||
hits = sorted(glob.glob(pattern))
|
||
return hits[-1] if hits else None
|
||
|
||
|
||
def tris_to_target_quads(tris):
|
||
"""三角面数换算 QR 目标四边形数:1 quad ≈ 2 tris,至少 1。"""
|
||
return max(1, int(tris) // 2)
|
||
|
||
|
||
def build_settings(in_fbx, out_fbx, prog, target_quads,
|
||
adaptive=50, exact=False, hard_edges=True):
|
||
"""生成 RetopoSettings.txt 文本(xremesh.exe -s 读取)。"""
|
||
lines = [
|
||
"HostApp=Blender",
|
||
'FileIn="%s"' % in_fbx,
|
||
'FileOut="%s"' % out_fbx,
|
||
'ProgressFile="%s"' % prog,
|
||
"TargetQuadCount=%d" % target_quads,
|
||
"CurvatureAdaptivness=%d" % adaptive,
|
||
"ExactQuadCount=%d" % (1 if exact else 0),
|
||
"UseVertexColorMap=False",
|
||
"UseMaterialIds=0",
|
||
"UseIndexedNormals=0",
|
||
"AutoDetectHardEdges=%d" % (1 if hard_edges else 0),
|
||
]
|
||
return "\n".join(lines) + "\n"
|
||
|
||
|
||
def parse_progress(text):
|
||
"""解析 progress.txt -> (state, msg)。
|
||
首行 '2'=success;<0=error(次行为文本);空/分数/非法=running。"""
|
||
lines = text.splitlines()
|
||
if not lines:
|
||
return ("running", "")
|
||
try:
|
||
v = float(lines[0])
|
||
except ValueError:
|
||
return ("running", "")
|
||
if v == 2:
|
||
return ("success", "")
|
||
if v < 0:
|
||
return ("error", lines[1] if len(lines) > 1 else "")
|
||
return ("running", "")
|
||
|
||
|
||
def topology_acceptable(src_boundary, src_nonmanifold,
|
||
out_boundary, out_nonmanifold):
|
||
"""QR 输出拓扑是否可接受:不比输入引入更多开边(孔洞)/非流形边。
|
||
QR 引擎在激进减面下会非确定性地打出孔洞——封闭输入(0/0)时要求输出也 0/0,
|
||
输入本身开放(壳)时按其基线放行;否则判破损,交上层回退 collapse。"""
|
||
return out_boundary <= src_boundary and out_nonmanifold <= src_nonmanifold
|