ModelTranslator: FBX 减面工具 model_decimate(Decimate collapse + Smart UV 重展)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8ede09f859
commit
65b4c3fe8e
@@ -22,3 +22,99 @@ def within_tolerance(target, actual, tol=TOLERANCE):
|
|||||||
if target <= 0:
|
if target <= 0:
|
||||||
return False
|
return False
|
||||||
return abs(actual - target) <= target * tol
|
return abs(actual - target) <= target * tol
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 以下仅在 Blender 内执行 ----------------
|
||||||
|
|
||||||
|
|
||||||
|
def join_meshes(objs):
|
||||||
|
"""多 mesh join 成单对象,返回结果对象(bl_bake 复用)。"""
|
||||||
|
import bpy
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for o in objs:
|
||||||
|
o.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = objs[0]
|
||||||
|
if len(objs) > 1:
|
||||||
|
bpy.ops.object.join()
|
||||||
|
return bpy.context.view_layer.objects.active
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_modifier(obj, mod):
|
||||||
|
import bpy
|
||||||
|
bpy.context.view_layer.objects.active = obj
|
||||||
|
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||||
|
|
||||||
|
|
||||||
|
def _triangulate(obj):
|
||||||
|
mod = obj.modifiers.new("mt_tri", 'TRIANGULATE')
|
||||||
|
_apply_modifier(obj, mod)
|
||||||
|
|
||||||
|
|
||||||
|
def _decimate(obj, ratio):
|
||||||
|
mod = obj.modifiers.new("mt_dec", 'DECIMATE')
|
||||||
|
mod.decimate_type = 'COLLAPSE'
|
||||||
|
mod.ratio = ratio
|
||||||
|
mod.use_collapse_triangulate = True
|
||||||
|
_apply_modifier(obj, mod)
|
||||||
|
|
||||||
|
|
||||||
|
def _smart_unwrap(obj):
|
||||||
|
"""删旧 UV 层后 Smart UV Project 重展。island_margin 0.002 ≈ 2048 图 4px。"""
|
||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
mesh = obj.data
|
||||||
|
while mesh.uv_layers:
|
||||||
|
mesh.uv_layers.remove(mesh.uv_layers[0])
|
||||||
|
bpy.context.view_layer.objects.active = obj
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.mesh.select_all(action='SELECT')
|
||||||
|
bpy.ops.uv.smart_project(angle_limit=math.radians(66.0), island_margin=0.002)
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import bpy
|
||||||
|
import json
|
||||||
|
argv = sys.argv[sys.argv.index("--") + 1:]
|
||||||
|
src, out_fbx, target = argv[0], argv[1], int(argv[2])
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
bpy.ops.import_scene.fbx(filepath=src)
|
||||||
|
|
||||||
|
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
|
||||||
|
skipped = [o.name for o in bpy.data.objects if o.type != 'MESH']
|
||||||
|
if skipped:
|
||||||
|
warnings.append("忽略非 mesh 对象:%s(本工具只处理静态网格)" % ", ".join(skipped))
|
||||||
|
if not meshes:
|
||||||
|
print("MT_SUMMARY " + json.dumps({"error": "FBX 中没有 mesh"}, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
obj = join_meshes(meshes)
|
||||||
|
|
||||||
|
_triangulate(obj)
|
||||||
|
orig = len(obj.data.polygons)
|
||||||
|
cur = orig
|
||||||
|
if cur <= target:
|
||||||
|
warnings.append("当前 %d 面 <= 目标 %d,跳过减面" % (cur, target))
|
||||||
|
else:
|
||||||
|
# collapse 结果是近似值;未达容差时按新面数修正 ratio 再来,最多 MAX_RETRY 轮
|
||||||
|
for _ in range(1 + MAX_RETRY):
|
||||||
|
_decimate(obj, decimate_ratio(target, cur))
|
||||||
|
_triangulate(obj) # collapse triangulate 后仍可能残留非三角面
|
||||||
|
cur = len(obj.data.polygons)
|
||||||
|
if within_tolerance(target, cur) or cur <= target:
|
||||||
|
break
|
||||||
|
|
||||||
|
_smart_unwrap(obj)
|
||||||
|
out_dir = os.path.dirname(os.path.abspath(out_fbx))
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
bpy.ops.export_scene.fbx(filepath=out_fbx, path_mode='STRIP', embed_textures=False)
|
||||||
|
|
||||||
|
print("MT_SUMMARY " + json.dumps(
|
||||||
|
{"src": os.path.basename(src), "fbx": os.path.basename(out_fbx),
|
||||||
|
"tris_before": orig, "tris_after": cur, "target": target,
|
||||||
|
"warnings": warnings}, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__" and "--" in sys.argv:
|
||||||
|
main()
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""FBX 减面 CLI:减到指定三角面数并 Smart UV 重展,输出 <名>_low.fbx。
|
||||||
|
用法:python model_decimate.py src/well1500.fbx --tris 5000 [-o dir] [--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("--blender", default=None)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
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")
|
||||||
|
s = run_blender_script(blender, "bl_decimate.py",
|
||||||
|
[args.input, out_fbx, str(args.tris)])
|
||||||
|
print("== %s: %d -> %d 面(目标 %d)-> %s" %
|
||||||
|
(s["src"], s["tris_before"], s["tris_after"], s["target"], out_fbx))
|
||||||
|
for w in s["warnings"]:
|
||||||
|
print(" [警告] %s" % w)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user