tripocli v2.0.0:Tripo AI 3D 生成与加工 CLI(面向 Agent 与开发者,纯 Python 标准库、零第三方依赖)
覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、 纹理、PBR、智能 UV、部件补全、格式导出与下载。 代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
This commit is contained in:
commit
977f97995a
60 files changed
+5622
No files matched your search
@@ -0,0 +1,185 @@
|
||||
"""本地游戏资产优化 — 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
|
||||
Reference in new issue
Block a user