覆盖文生 3D、图生 3D、多视图、批量生成,以及重拓扑、绑定、动画、智能分割、 纹理、PBR、智能 UV、部件补全、格式导出与下载。 代码组织:一个功能一个模块(tripo/features/),入口薄壳 tripocli.py。
121 lines
4.7 KiB
Python
121 lines
4.7 KiB
Python
"""动画 — retarget a rigged model onto animation presets.
|
||
|
||
tripocli animate -p <project_id> --preset idle --wait -o anim_idle.glb
|
||
tripocli animate --list
|
||
"""
|
||
|
||
import os
|
||
|
||
from . import _common
|
||
from .. import motions
|
||
|
||
NAME = "animate"
|
||
HELP = "动画:为已绑定骨骼的模型套用动作预设"
|
||
|
||
PRESET_EXAMPLES = ["idle", "walk", "run", "slash", "cast_a_spell", "jump"]
|
||
|
||
|
||
def add_arguments(parser):
|
||
parser.add_argument("-p", "--project", help="项目 ID(--list 时可省略)")
|
||
parser.add_argument("-m", "--model", help="覆盖 model_version(默认 default)")
|
||
_common.add_wait_options(parser, "动画结果保存到该路径(可选)")
|
||
parser.add_argument("--type", default="biped", help="骨骼类型,默认 biped")
|
||
parser.add_argument(
|
||
"--preset", action="append",
|
||
help="动作预设名(如 idle / walk / run / slash),可重复;用 --list 查看全部",
|
||
)
|
||
parser.add_argument(
|
||
"--animation", action="append",
|
||
help="完整动作标识(如 preset:biped:idle),可重复",
|
||
)
|
||
parser.add_argument(
|
||
"--motion-asset",
|
||
help="套用「文字生成动作」产出的动作资产 ID(来自 tripocli motion)",
|
||
)
|
||
parser.add_argument("--list", action="store_true", help="列出全部可用动作预设(按骨骼类型)")
|
||
|
||
|
||
def run(ctx):
|
||
args = ctx.args
|
||
|
||
if args.list:
|
||
data = {
|
||
rig: [
|
||
{"name": item["name"], "action_type": item.get("action_type"),
|
||
"category": item.get("category"), "id": motions.identifier(rig, item["name"])}
|
||
for item in motions.catalog().get(rig, [])
|
||
]
|
||
for rig in motions.rig_types()
|
||
}
|
||
|
||
def human():
|
||
for rig, items in data.items():
|
||
ctx.out.line(f"== {rig} ({len(items)}) ==")
|
||
ctx.out.line(" " + ", ".join(item["name"] for item in items))
|
||
|
||
ctx.out.data(data, human=human)
|
||
return data
|
||
|
||
animations = list(args.animation or [])
|
||
for preset in (args.preset or []):
|
||
animations.append(preset if ":" in preset else motions.identifier(args.type, preset))
|
||
if not args.project:
|
||
raise ValueError("需要 -p/--project 指定项目(用 --list 只查看动作清单)")
|
||
|
||
body = {
|
||
"project_id": args.project,
|
||
"rig_type": args.type,
|
||
"model_version": args.model or "default",
|
||
}
|
||
if args.motion_asset:
|
||
# 文字生成动作产出的资产:retarget_model 用 motion_asset_id(而不是 animations)
|
||
body["motion_asset_id"] = args.motion_asset
|
||
elif animations:
|
||
body["animations"] = animations
|
||
else:
|
||
raise ValueError(
|
||
"至少需要一个动作:用 --preset afraid、--animation preset:biped:afraid,"
|
||
f"或 --motion-asset <id>(示例: {', '.join(PRESET_EXAMPLES)};用 --list 看全部)"
|
||
)
|
||
|
||
data = ctx.client.get_data("POST", "/v2/studio/operation/retarget_model", body=body)
|
||
if not args.wait:
|
||
return _common.run_operator(ctx, data, output=args.output, wait=False, label="动画重定向")
|
||
|
||
operator_id = (data or {}).get("operator_id")
|
||
project_id = (data or {}).get("project_id") or args.project
|
||
if operator_id:
|
||
_common.wait_and_report(ctx, operator_id, timeout=900)
|
||
|
||
# 动画结果不在 project.model_url(那是绑骨结果),而在 operator.retarget[] 里:
|
||
# 每个元素 {operator_id, name:"preset:biped:xxx", status, model_url}(实测确认)。
|
||
entry = _retarget_entry(ctx.client, project_id, operator_id)
|
||
model_url = (entry or {}).get("model_url")
|
||
output_path = args.output
|
||
if output_path and model_url:
|
||
ctx.client.download(model_url, output_path)
|
||
output_path = os.path.abspath(output_path)
|
||
|
||
payload = {"result": data, "animation": (entry or {}).get("name"), "model_url": model_url,
|
||
"output_file": output_path if model_url else None}
|
||
ctx.out.data(
|
||
payload,
|
||
human=lambda: (
|
||
ctx.out.line(f"🎉 动画重定向完成({(entry or {}).get('name')})"),
|
||
ctx.out.line(f"模型地址: {model_url}"),
|
||
ctx.out.line(f"已保存 : {output_path}") if model_url else ctx.out.line("⚠️ 未取到动画 model_url"),
|
||
),
|
||
)
|
||
return payload
|
||
|
||
|
||
def _retarget_entry(client, project_id, operator_id=None):
|
||
"""Pick the retarget result from the project's operator.retarget list."""
|
||
detail = client.project_detail(project_id) or {}
|
||
entries = (detail.get("operator") or {}).get("retarget") or []
|
||
if operator_id:
|
||
for item in entries:
|
||
if item.get("operator_id") == operator_id:
|
||
return item
|
||
return entries[-1] if entries else None
|