覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、 纹理、PBR、智能 UV、部件补全、格式导出与下载。 代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
186 lines
7.2 KiB
Python
186 lines
7.2 KiB
Python
"""本地游戏资产优化 — 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
|