Files
Mrpzx 977f97995a tripocli v2.0.0:Tripo AI 3D 生成与加工 CLI(面向 Agent 与开发者,纯 Python 标准库、零第三方依赖)
覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、
纹理、PBR、智能 UV、部件补全、格式导出与下载。

代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
2026-10-13 19:22:54 +08:00

186 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""本地游戏资产优化 — 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