"""Blender 内运行:FBX 减面到指定三角面数 + Smart UV 重展。 调用:blender -b --factory-startup --python bl_decimate.py -- """ import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) TOLERANCE = 0.03 # 面数相对误差容忍 MAX_RETRY = 2 # ratio 修正轮数上限 def decimate_ratio(target, cur): """Decimate collapse ratio;cur <= target 或非法时返回 1.0(不减面)。""" if cur <= 0 or cur <= target: return 1.0 return target / float(cur) def within_tolerance(target, actual, tol=TOLERANCE): """减面结果是否落在目标 ±tol 内。""" 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 # 重试用尽仍超差且偏多:记录警告,交由上层决定是否可接受 if not within_tolerance(target, cur) and cur > target: warnings.append("修正 %d 轮后仍超差:%d 面(目标 %d ±3%%)" % (MAX_RETRY, cur, target)) _smart_unwrap(obj) out_dir = os.path.dirname(os.path.abspath(out_fbx)) os.makedirs(out_dir, exist_ok=True) # 仅导出合并后的对象;整场景导出会把 armature/empty 等非 mesh 带进 _low.fbx bpy.ops.export_scene.fbx(filepath=out_fbx, use_selection=True, 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()