"""本地游戏资产优化 — decimate triangles, resize textures, Draco-compress, and build LODs. tripocli optimize house.glb -o house_game.glb --faces 15000 tripocli optimize hero.glb --in-place --faces 40000 tripocli optimize house.glb --lods """ import os import shutil import subprocess import tempfile from .. import glb NAME = "optimize" HELP = "本地模型优化:真减面、锁 UV、贴图缩放、Draco 压缩与一键生成多级 LOD(0 积分)" def add_arguments(parser): parser.add_argument("input", help="输入的本地 .glb 文件路径") parser.add_argument("-o", "--output", help="输出 .glb 路径(默认 <原文件名>_opt.glb)") parser.add_argument("--in-place", action="store_true", help="直接原地覆盖输入文件") parser.add_argument( "--faces", type=int, help="目标三角形面数(如 15000;不传则按静态模型 18000 / 骨骼角色 45000 自动推算)", ) parser.add_argument("--ratio", type=float, help="减面保留比例 0~1(优先于 --faces)") parser.add_argument( "--error", type=float, help="几何误差容忍度(默认静态模型 0.06,带骨骼角色 0.02 以保护五官与关节)", ) parser.add_argument( "--texture-size", type=int, default=1024, help="限制最大贴图边长(默认 1024;传 0 保留原始尺寸)", ) parser.add_argument("--no-draco", action="store_true", help="跳过 Draco 几何压缩") parser.add_argument( "--lods", action="store_true", help="一键生成 3 级游戏 LOD 文件(<名>_lod0.glb / _lod1.glb / _lod2.glb)", ) def _runner(): if shutil.which("gltf-transform"): return ["gltf-transform"] if shutil.which("npx"): return ["npx", "--yes", "@gltf-transform/cli"] raise RuntimeError("未找到 npx 或 gltf-transform,请先安装 Node.js") def _run_cmd(cmd): proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode != 0: detail = (proc.stderr or proc.stdout or "").strip()[-600:] raise RuntimeError(f"执行失败 ({' '.join(cmd[:4])}): {detail}") def _optimize_one(src, dst, before_info, target_faces=None, ratio=None, error=None, texture_size=None, use_draco=True): cli = _runner() is_rigged = before_info["is_rigged"] orig_tris = max(before_info["triangles"], 1) if ratio is None: faces = target_faces or (45000 if is_rigged else 18000) ratio = min(1.0, round(faces / orig_tris, 4)) err = error if error is not None else (0.02 if is_rigged else 0.06) with tempfile.TemporaryDirectory(prefix="tripo_opt_") as tmpdir: current = src if ratio < 0.999 and before_info["meshes"] > 0: step_weld = os.path.join(tmpdir, "welded.glb") _run_cmd(cli + ["weld", current, step_weld]) step_simp = os.path.join(tmpdir, "simplified.glb") _run_cmd(cli + ["simplify", step_weld, step_simp, "--ratio", str(ratio), "--error", str(err)]) current = step_simp if texture_size and texture_size > 0 and before_info["textures"] > 0: step_tex = os.path.join(tmpdir, "resized.glb") _run_cmd(cli + [ "resize", current, step_tex, "--width", str(texture_size), "--height", str(texture_size), ]) current = step_tex if use_draco and before_info["meshes"] > 0: step_draco = os.path.join(tmpdir, "draco.glb") _run_cmd(cli + ["draco", current, step_draco]) current = step_draco os.makedirs(os.path.dirname(os.path.abspath(dst)) or ".", exist_ok=True) shutil.copy2(current, dst) after_info = glb.inspect_file(dst) tri_drop = round((1.0 - after_info["triangles"] / orig_tris) * 100, 1) if orig_tris else 0.0 size_drop = round((1.0 - after_info["file_size"] / max(before_info["file_size"], 1)) * 100, 1) return { "output_file": os.path.abspath(dst), "before_triangles": before_info["triangles"], "after_triangles": after_info["triangles"], "triangle_reduction_pct": tri_drop, "before_size_mb": before_info["file_size_mb"], "after_size_mb": after_info["file_size_mb"], "size_reduction_pct": size_drop, "ratio_used": ratio, "error_used": err, } def run(ctx): args = ctx.args src = os.path.abspath(args.input) if not os.path.exists(src): raise FileNotFoundError(f"文件不存在: {src}") before = glb.inspect_file(src) stem, ext = os.path.splitext(src) use_draco = not args.no_draco if args.lods: base_out = os.path.splitext(os.path.abspath(args.output))[0] if args.output else stem tiers = [ ("lod0", args.faces or (45000 if before["is_rigged"] else 20000), 0.03 if before["is_rigged"] else 0.05), ("lod1", 12000 if before["is_rigged"] else 5000, 0.05 if before["is_rigged"] else 0.08), ("lod2", 3500 if before["is_rigged"] else 1500, 0.10 if before["is_rigged"] else 0.15), ] lods = [] for tag, faces, err in tiers: target_path = f"{base_out}_{tag}{ext or '.glb'}" ctx.out.line(f"⚙️ 生成 {tag.upper()} (目标 ≤{faces:,} 面) -> {os.path.basename(target_path)}...") report = _optimize_one( src, target_path, before, target_faces=faces, error=err, texture_size=args.texture_size, use_draco=use_draco, ) report["lod"] = tag lods.append(report) payload = {"input": src, "before_triangles": before["triangles"], "lods": lods} ctx.out.data( payload, human=lambda: [ ctx.out.line( f"✅ {item['lod'].upper()}: {item['before_triangles']:,} -> {item['after_triangles']:,} 三角面 " f"(-{item['triangle_reduction_pct']}%), {item['before_size_mb']}MB -> {item['after_size_mb']}MB " f"({item['output_file']})" ) for item in lods ], ) return payload if args.in_place: dst = src elif args.output: dst = os.path.abspath(args.output) else: dst = f"{stem}_opt{ext or '.glb'}" ctx.out.line( f"⚙️ 优化模型: {os.path.basename(src)} " f"(原始 {before['triangles']:,} 三角面, {before['file_size_mb']} MB" f"{', 带骨骼蒙皮' if before['is_rigged'] else ''})..." ) report = _optimize_one( src, dst, before, target_faces=args.faces, ratio=args.ratio, error=args.error, texture_size=args.texture_size, use_draco=use_draco, ) ctx.out.data( report, human=lambda: ( ctx.out.line( f"🎉 优化完成: {report['before_triangles']:,} -> {report['after_triangles']:,} 三角面 " f"(-{report['triangle_reduction_pct']}%)" ), ctx.out.line( f" 体积变化: {report['before_size_mb']} MB -> {report['after_size_mb']} MB " f"(-{report['size_reduction_pct']}%)" ), ctx.out.line(f" 已保存 : {report['output_file']}"), ), ) return report