From 65b4c3fe8e4302411a26452b79e7a158920cdea0 Mon Sep 17 00:00:00 2001 From: ud18010 Date: Thu, 16 Jul 2026 10:39:16 +0800 Subject: [PATCH] =?UTF-8?q?ModelTranslator:=20FBX=20=E5=87=8F=E9=9D=A2?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=20model=5Fdecimate=EF=BC=88Decimate=20collap?= =?UTF-8?q?se=20+=20Smart=20UV=20=E9=87=8D=E5=B1=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- Tools/ModelTranslator/bl_decimate.py | 96 +++++++++++++++++++++++++ Tools/ModelTranslator/model_decimate.py | 30 ++++++++ 2 files changed, 126 insertions(+) create mode 100644 Tools/ModelTranslator/model_decimate.py diff --git a/Tools/ModelTranslator/bl_decimate.py b/Tools/ModelTranslator/bl_decimate.py index e2d0df12..f1a68209 100644 --- a/Tools/ModelTranslator/bl_decimate.py +++ b/Tools/ModelTranslator/bl_decimate.py @@ -22,3 +22,99 @@ def within_tolerance(target, actual, tol=TOLERANCE): if target <= 0: return False 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() diff --git a/Tools/ModelTranslator/model_decimate.py b/Tools/ModelTranslator/model_decimate.py new file mode 100644 index 00000000..4c3d9d1c --- /dev/null +++ b/Tools/ModelTranslator/model_decimate.py @@ -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()