关于一些Pipeline开发分享

引言

在大多数VFX团队中,Pipeline并不是被设计出来的,而是“逐渐长出来的”。

一开始,只是几个脚本,用来解决具体问题;
后来变成一堆工具,分布在不同DCC里。

再往后,问题开始浮现

  • 之前用过一个工具怎么找不到了:工具入口混乱
  • 为什么发布后的缓存跟我拍屏对不上:数据不一致
  • 发布任务卡着不动,不知道是否“死”掉了:任务可观测性缺失

这时候意识到问题:
Pipeline并不是工具集合,而是一个“约束生产流程的数据系统”。

但这些认知,并不是一开始就想清楚的,而是在一次次具体问题中被“逼”出来的。
这篇文章是结合一些实际开发中的问题和解决方式,分享在Pipeline演进过程中的一些经验。

基建: TD也需要自己的Pipeline

我们习惯为制作人员设计Pipeline,但很少有人认真思考一件事:
TD团队本身,也需要一套自己的Pipeline。

记得刚转到开发岗时,我的主管就是一位专门开发 Core Pipeline 的TD。
他并不直接写面向Artist的工具,而是为我们这些TD提供“基础能力”,定义TD应该如何工作。
并把这种工作方式工具化、标准化。

将工作流工具化

当时团队里有一套工具,它基于 GitLab Flow,通过一组命令行,把整个开发工作流串了起来。

当我们接到一个需求时,不是手动去找项目、拉代码、建分支、开IDE。
而是执行:

1
pipe setup

工具会进入一个问询式对话:

1
2
请输入 Jira 工单号:
请输入要修改的 GitLab 项目:

然后它会自动完成:

1
2
3
4
5
初始化本地 workspace
拉取对应仓库
创建的开发分支
绑定 Jira 工单号
打开 IDE

在开发的过程中可以

1
2
3
pipe check  # 调用 ruff 检查代码规范
pipe update # 为workspace所有仓库抓取更新
pipe test # 创建测试环境

这些命令看起来只是简化了日常操作,但它真正解决的,是把一整套开发流程变成了“默认行为”。

从需求认领、环境初始化,到代码检查、测试环境创建,
每一步都不再依赖个人习惯,而是由工具统一约束。

标准化通用函数

一个常见需求,通过当前文件路径找到当前任务是什么,并获取任务id。

如果不提供标准方法,通常是:
自己解析路径、手动拼规则、再去调用 Shotgun 查询,每个人都有自己的一套实现方式。
这样一来,一旦底层规则发生变化,每个人都需要去修改各自的代码来适配。

对这样的需求可以实现一个通用函数供大家调用

1
2
3
4
5
6
7
8
from pipeline.core import get_context_from_path

context = get_context_from_path(file_path)

print(context.task.name) # 当前任务名
print(context.task.next_version) # 当前任务的下一个版本
print(context.task.id) # 当前任务的id
print(context.entity.name) # 镜头号

除此之外,对于日常频繁打交道的系统,比如 Shotgun、Deadline,
我们也可以在它们之上再封装一层,作为 TD 自己的工具。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import sgi  # Shotgun模块封装

# 创建一个rnd项目的实例对象
project = sgi.Project(name='rnd')

# 返回它的ID
print(project.id)

# 返回指定字段的值
print(project.field('sg_status'))

# 获取项目下所有的场次,由sgi.Sequence对象组成的列表
print(project.sequences)

for seq in project.sequences:
print(seq.name)

有了这些标准化的通用方法,每个TD不再需要关心“规则是如何实现的”,
只需要直接使用“规则本身”。

可配置的Pipeline

记得有一次,下班刚背上包准备走,制作同事跑过来说检查项过不去。
但这个文件因为一些特殊原因,需要临时跳过某个检查项。

问题在于,我们的检查规则是写死在代码里的。
如果要调整,只能走一遍完整的代码发布流程。

而且这还只是一个镜头的临时需求:
我需要为它“开一次口子”,等它发布完,还得再把代码改回去。

很多检查项是放在yaml/json文件里,这些都是随Git部署在服务中,没法灵活修改。

后来开始尝试把这些配置从代码中抽离出来,托管到数据库中。

image.png

有了这一次尝试,会发现:
不只是检查项,Pipeline中其实有大量“适合被配置化”的内容。
比如DCC的启动配置,每个工具的启动,本质上都是一条命令:

  • Maya2023: rez-env maya-2023 maya_extend mtoa -- maya
  • Houdini20.5: rez-env houdini-20.5 houdini_extend htoa -- hfs
  • xxTool: rez-env xxtool -- run

还可用作全局控制开关

  • 是否允许Publish:当存储服务器出现问题,暂时关闭发布入口。
  • 是否允许提交Deadline:当Deadline升级时,暂时关闭提交入口。
  • 某个模块的版本:指定版本/latest

工具体验的一致性与效率优化

统一UI风格

当用户打开不同工具时,如果每个界面风格、交互方式都不一致,
就意味着他们每次都要重新学习一遍。

因此,我们会尽量统一:

  • 界面布局(标题/简单说明/帮助文档跳转按钮)
  • 状态反馈(成功 / 失败 / 过程提示)

我自己常用的工具布局大概是这样:
image.png

然后基于这个布局,抽象出一个 CommonToolDialog 基类。
封装一些通用能力,之后开发的每个工具,只需要继承这个基类,
专注实现自己的业务逻辑,而不需要重复搭建 UI。

image.png

这样用户也会逐渐形成稳定的使用习惯,工具之间的差异被降低,使用成本也随之降低,
最终形成一套无需说明即可上手的使用体验。

工具入口

不少公司的工具还是习惯被集中放置在软件菜单中。
但随着数量不断增加,菜单层级变深、条目变多,用户在查找某个工具时需要逐项扫视,效率较低。

image.png

后来我们转向使用“工具库”的形式,提供了分类,搜索,收藏等功能。
通过快捷键打开工具面板(吸附在侧边栏),默认焦点放在搜索栏,
输入关键字就可以快速定位到要使用工具。

image.png
(图片由AI生成)

对这样的工具库我们还可以做一些优化,比如使用类似Quick Launcher聚焦搜索来简化调用方式

image.png
(图片由AI生成)

甚至也可以使用AI为它进行赋能,为每个工具写一份metadata定义,当用户询问“我要将动画导出到别的镜头”,
工具库就会按照相似度排序,在搜索结果在显示满足需要的工具。

(这真的很有必要,公司人员总是在流动,常常会被问到”我们有xxx工具吗?” )

速度优化

在之前一家公司,我们使用的是Shotgun提供的集成框架Toolkit。
自然就会用到SG流程三件套: Loader,Publisher,Workfiles。

image.png

这些工具本身提供了完整的流程能力,但由于 ShotGrid 采用远程服务的方式,
每次打开工具通常需要 2-3 秒,在网络不稳定的情况下甚至会出现卡顿或无响应。

一开始大家还能接受,但随着使用频率增加,这种“每次都要等一下”的体验会被不断放大,
最终带来的结果是:制作人员逐渐放弃使用工具,转而采用手动操作。

但问题在于,Pipeline 中的很多操作其实并不是“简单操作”。

例如一个看似简单的“导入资产”,在工具内部往往包含了多步逻辑:

  • 创建规范的层级结构
  • 绑定正确的资产路径
  • 写入自定义属性
  • 锁定关键节点,防止误操作

当用户绕过工具,直接手动导入时,这些隐含的流程约束就会全部丢失,
从而带来数据不规范、流程不可控等一系列问题。

当时的解决方式是: 抛弃掉SG提供的app,自己开发三件套。
写完后发现打开速度是比之前快不少,但获取SG数据本身还是使用shotgun_api3请求远程数据库,
在获取TaskVersion数据时还是会有1-2秒的“卡顿”。

后来在网上翻到了“把SG数据缓存到本地数据库”的方案。

核心做法是

  • 1.建表: 根据 Shotgun schema 在 PostgreSQL 中自动创建所有实体对应的表和字段
  • 2.全量同步SG数据到数据库
  • 3.使用 Shotgun event daemon 监听事件变更同步
  • 4.提供一个查询缓存的接口

改造完成后,用户体验有了明显提升,整个使用过程从“每一步都要等一下”,变成了几乎无感的实时交互。

在开发过程中,把自己想象成使用者

如何让使用者不排斥工具,愿意主动使用;
让用户能快速找到工具;
降低学习成本,让新工具能够被快速理解和上手;

这些都是在设计和开发过程中持续思考的问题。

数据留存

为了让 Pipeline 具备可追溯性和可分析能力,我们在各个关键环节尽量记录更多数据。

发布数据增强

之前遇到过一个典型问题:
由于检查逻辑不完善,导致部分模型的绑定关系与点序不匹配。

作为 TD,需要快速筛出所有存在问题的资产。但当资产规模达到上百个时,
如果逐个打开文件、读取点序再进行比对,这种方式显然是低效的。

当时想着,如果能在发布时,将关键结构信息(例如点序、面数等)写入一个结构化文件(如 YAML),
从而将原本需要逐个打开文件的操作,转化为对文本数据的快速比对,这个文件可以像是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
asset:
name: baisc_man
type: character
department: model

geometry:
mesh_count: 128
total_faces: 35672
total_vertices: 18234

meshes:
- name: body_geo
faces: 18234
vertices: 9211
topology_hash: "a8f3c2d9d46qw1dw5cc"
uv_sets: ["map1"]
- name: head_geo
faces: 8421
vertices: 4210
topology_hash: "c91de77aqd2234dasc"
uv_sets: ["map1", "uv_face"]

checks:
topology_consistency: true
non_manifold: false
uv_out_of_range: false
warnings: []

dependencies:
textures:
- /proj/rnd/assets/char/baisc_man/tex/v008/body_diff.tx
- /proj/rnd/assets/char/baisc_man/tex/v008/head_diff.tx
references: []

metadata:
tool_version: 1.3.0
publish_type: model
notes: "auto publish from model tool"

所以在发布阶段,不仅仅是输出文件,尽量记录更多上下文信息,能对我们的排错工作提供很大帮助。

历史记录

运行过程中的日志非常重要,它不仅用于排错,更用于溯源。

因为大多数情况下,当问题被发现时,已经是一个“结果状态”:
文件已经生成、场景已经被修改,问题已经发生。

一个典型场景:
当你发现文件中的某个资产缺少关键属性时,
很容易怀疑是制作过程中没有正确使用工具进行引用。

但制作人员往往会说:
“我就是按流程操作的,也不知道为什么会变成这样。”

这个时候,问题就变成了一个悬案。

  • 工具是否被正确使用?
  • 使用时传入了什么参数?
  • 中间是否有异常或被中断?
  • 是否有人手动修改了结果?

有日志就像是监控,当问题发生时,我们不再依赖人的记忆,而是依赖数据本身。

使用统计

这源于某天的好奇,我们开发了这么多工具,制作都在用吗?什么是他们常用的?
这其实对于我做一些判断非常有用,实现起来也不难,写个装饰器,记到数据库里就行。

image.png

最后

为什么要写这一篇?因为最近在维护一个比较老的流程,
在修修补补的过程中,会不断想起以前做过的一些好的方案。

在现在的动画影视行业里,留给 TD 配额其实不多,
一个人往往要同时负责好几个环节。

大多数时间,我们都在做“修补”。
很多问题其实心里很清楚根源在哪,
但现实里,很难真的从底层去改掉它。

所以就先把这些经验记录下来,分享出来。
也希望这个行业,能慢慢变得好一点。