背景:同一件事,不同写法
在 CG 制作里,“序列帧命名”看起来只是一个小细节,但它会贯穿渲染输出、合成读写、缓存交换、以及自动化发布等很多环节。
只要你做过跨软件协作,就一定遇到过类似下面这种路径:
shot010_lgt_v003.0001.exr
shot010_lgt_v003.0002.exr
...为了让软件在输出或读取时能自动替换帧号,不同工具演化出了不同的“占位符”语法:Houdini 常见 $F4,Nuke 常见 ####,而 Unity Recorder 之类的工具又会写成 {FrameID:4}。
下面按三种典型写法把来龙去脉理一遍。
三类常见占位符
1) $F / $F4:Unix 变量习惯的延续
- 代表:Houdini(HScript)、部分 Maya/渲染器工具链等
- 常见写法:
$F、$F4、$T
这类写法非常像 Shell 里的环境变量:用 $ 引用变量值。
以 $F4 为例,可以理解成“帧号(Frame)以 4 位输出,不足补 0”。
shot_v01.$F4.exr -> shot_v01.0001.exr它的优点是简洁,缺点是“变量边界”不总是显式的:当字符串拼接得很复杂时,读起来不如后面两种直观。
2) #### / %04d:更“可视化”的占位
- 代表:Nuke、Maya、3ds Max、Deadline 等
- 常见写法:
####、%04d
#### 是一种非常直观的写法:一个 # 代表一位数字,占位位数一眼就能看出来。
%04d 则来自 C 语言 printf 风格的格式化字符串:0 表示补零,4 表示宽度,d 表示整数。
out_%04d.jpg -> out_0001.jpg3) {FrameID:4}:更偏“软件工程”的格式化
- 代表:Unity(Recorder)、Unreal Engine(MRQ/工具链中常见)
- 常见写法:
{FrameID:4}、{frame_number}等
这种写法更接近现代语言的“格式化/插值”机制:用大括号明确标出变量边界,并允许在冒号后附带格式规则(比如位宽、日期格式等)。
Shot_{FrameID:4}.png -> Shot_0001.png快速对比表
| 工具 | 语法示例 | 逻辑分类 | 核心驱动 |
|---|---|---|---|
| Houdini | shot_v01.$F4.exr | 变量表达式 | HScript |
| Nuke | shot_v01.####.exr | 视觉通配符 | C-style / TCL |
| Unity | Shot_{FrameID:4}.png | 复合格式化 | C# / .NET |
| UE5 | {sequence_name}_{frame} | 键值对映射 | Python / C++ |
| FFmpeg | out_%04d.jpg | 格式化说明符 | C Library |
为什么 {} 写法越来越常见
$F4 或 #### 本质上都是“约定式”的替换规则:短、快,但也更容易在复杂字符串里产生歧义。
而 {FrameID:4} 这类写法的优势主要在两点:
- 边界清晰:变量从
{到}一眼就能确定范围,减少误判。 - 更容易扩展:同一个机制可以支持更多格式化能力,而不需要反复新增解析规则。
在做跨软件自动化时,建议把“占位符解析”当成一个独立的能力做成小模块(哪怕只有几十行),不要在各个脚本里散落一堆字符串替换。
TA / Pipeline 对接时的常见坑
1) 起始帧:1-based vs 0-based
有些工具默认第一帧是 1,有些场景会出现从 0 开始的序列。占位符解决的是“格式”,不是“帧范围”,这两个概念不要混在一起。
2) Padding:到底要 4 位还是 5 位
电影管线里 4 位是常见默认值,但长片/长剧也会遇到 5 位。
不要把位宽写死在脚本里,尽量做到从配置或上游信息读取。
3) 占位符识别:别只做字符串 replace
如果你要在中间层统一处理多种语法,建议用正则先“识别类型”,再做标准化。
import re
PATTERNS = [
("houdini", re.compile(r"\\$F(?P<padding>\\d+)?")),
("hashes", re.compile(r"(?P<hashes>#+)")),
("printf", re.compile(r"%(?P<padding>\\d+)d")),
("braces", re.compile(r"\\{\\s*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\\s*:\\s*(?P<padding>\\d+)\\s*\\}")),
]
def detect_placeholder(s: str):
for name, p in PATTERNS:
m = p.search(s)
if m:
return name, m.groupdict()
return None, {}结语
序列帧占位符的写法没有绝对的“好坏”,更多是各个时代、各类工具链的选择与历史包袱。
对我们做流程的人来说,关键不是站队哪一种,而是:能识别、能转换、能在团队里形成一致的约定。