t3 时间覆盖机制(Time Overrides):让子图拥有独立于全局时间轴的时钟
【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3
本篇技术文章基于 t3 仓库帮助文档 TimeOverrides.md 展开。t3(TiXL)是一个用于创建实时动态图形(realtime motion graphics)的开源节点式软件,其核心调度围绕"时间"进行;时间覆盖(Time Override)是其中一种关键机制——它允许你给某个算子或子图喂入自己的一路时间,而不是全局播放时间,从而独立地擦洗(scrub)、循环(loop)、冻结(freeze)或重映射(remap)项目中的某一段。读完后,你将理解 t3 中"全局时间 vs 局部时间"的双时间体系、TimeClip的仿射时间映射算法,以及如何用时间信号与时间剪辑(Time Clip)实现倒放、静止保持和非线性时间跳转,并能定位到仓库中对应的核心源码实现。
一、什么是时间覆盖:文档给出的核心定义
t3 帮助文档对该主题的完整表述是:
A way to feed an operator or sub-graph its own time instead of the global playback time — so you can scrub, loop, freeze, or remap a section of your project independently. Wrap a part of the graph so its time comes from a signal you supply — a sine, a remapped random, a beat — and everything inside animates against that instead of the timeline.
翻译过来即:时间覆盖是一种让算子或子图使用自定义时间、而非全局播放时间(playback time)的方式,目的是让你可以独立地擦洗、循环、冻结或重映射项目中的某一段。把图中的一部分包起来,让它的时间来自你提供的一路信号——正弦波、重映射后的随机数、节拍——那么其中的所有内容都会相对于这路信号(而不是时间轴)进行动画。文档还给出了典型用途:让场景倒着播、让它静止不动、或非线性地跳着穿过一段动画。
文档同时指出,时间轴上的时间剪辑(Time Clips)本质上做的是同一件事:每个剪辑"门控(gates)"图中哪一部分会被求值,并重映射喂入其子图的时间;橙色(orange)和红色(red)的视觉提示分别表示时间正在被重映射或被缩放。
下面结合仓库源码,把这条机制拆成两层:全局/局部时间的底层定义,以及两种具体的时间覆盖手段。
二、双时间体系:Playback全局时间与context.LocalTime局部时间
要理解时间覆盖,先要理解 t3 的时间术语体系。Playback.cs 文件头部的注释给出了权威说明:
- Time vs TimeInSecs:除非带
*InSecs后缀,默认时间单位是"小节(bar)"。在 120 BPM 下,一个 bar 单位等于 2 秒。换算关系由BarsFromSeconds/SecondsFromBars实现,公式为secs * Bpm / 240.0(Playback.cs)。 - Local vs Playback:"Local time 可以被算子覆盖(如 SetCommandTime 这类算子)和
TimeClip"。文档建议绝大多数算子使用 local time;而全局 Playback 时间用于让更新类算子(如Pulsate、Counter)即使在时间被覆盖的子图中也保持一致、可预测的时序。 - Time vs FxTime:
FxTime(效果时间)在开启 Idle Motion(空闲继续播放)时仍会前进,大多数特效类算子应使用它;Time则用于 UI 交互与一切由关键帧驱动的内容。
在 EvaluationContext.cs 中可以看到默认行为:求值上下文初始化时LocalTime = Playback.TimeInBars,即"局部时间默认等于全局播放时间"——时间覆盖要做的,正是在求值子图之前把这个值替换掉。
Playback.Update()每帧推进TimeInBars(播放时按timeSinceLastFrame * PlaybackSpeed * Bpm / 240.0累加),并支持LoopRange循环回绕(Playback.cs)。时间覆盖机制就构建在这套基础设施之上。
三、手段一:用信号驱动子图时间
文档中"wrap a part of the graph so its time comes from a signal you supply" 对应的是"把子图包在一个算子里、由外部信号决定其时间"的用法。仓库中有两个代表性算子。
3.1Time算子:读取多种时间源
Time.cs 实现了一个时间读取算子,其Mode输入支持五种时间模式(TimeModes枚举):
| 模式 | 取值来源 | 语义 |
|---|---|---|
LocalIdleMotionFxTime | context.LocalFxTime | 局部效果时间(Idle Motion 下仍前进) |
LocalTime | context.LocalTime | 局部时间——已被时间覆盖时就是覆盖后的时间 |
PlaybackTime | context.Playback.TimeInBars | 全局播放时间,不受局部覆盖影响 |
Runtime | 应用运行以来的秒数换算成 bar | 启动后单调递增 |
Frozen | 恒为 0 | 冻结,且会切换输出槽的 dirty flag 触发方式为None以避免无谓下游更新 |
它还提供Units输入(Bars/Secs)和SpeedFactor输入做线性缩放。关键细节是 Time.cs 中:当切换到Frozen模式时,算子会把输出槽的DirtyFlagTrigger从Animated切成None——冻结时间不再产生动画失效,从而避免下游整棵子图每帧重算。这正是文档所说 "freeze a section" 的工程落地。
注意LocalTime与PlaybackTime的分野:如果某个算子运行在被覆盖时间的子图内部,Time算子选LocalTime得到的就是覆盖后的时间,选PlaybackTime得到的仍是全局时间轴——这决定了"子图内部到底跟着谁动"。
3.2SetPlaybackTime算子:改写全局时钟
SetPlaybackTime.cs 是另一个方向的时间操纵器:它直接写入全局时钟。其输入端口(可对照 SetPlaybackTime.t3 中的默认值):
SubGraph(Command类型):待"包裹"的子图;TimeInBars(float,默认 0.0):目标时间,单位 bar;TriggerMode(默认OnceEnabledGetsTrue,另一档为Continuously);Enabled(bool):触发开关;ShowLogMessages(bool,默认 false):是否在日志中打印 "Setting playback time to …"。
其执行逻辑(SetPlaybackTime.cs)值得注意:
- 对 NaN / Infinity 的输入时间做防御,直接归零;
- 仅当
Enabled上升沿触发(MathUtils.WasTriggered)或处于Continuously模式时才写入; - 写入目标是
Playback.Current.TimeInBars,并会在没有活动 Playback 时记录警告 "Can't set playback time without active Playback"。
结合文档的用法提示,可以这样组合:
- 倒放场景:给
TimeInBars输入一个随真实时间递减的信号(例如起点 - Runtime),子图就会反向穿过动画; - 非线性跳转:把正弦波、重映射随机数或节拍相位(仓库中 BarPhaseTracker.cs、BeatSynchronizer.cs 等音频分析组件输出的相位/拍号信号)接到时间输入,子图即按该节奏重放;
- 保持静止:用
Time算子的Frozen模式,或让SetPlaybackTime持续写入同一个值。
需要区分的是:SetPlaybackTime改的是全局时钟,影响所有读取PlaybackTime的内容;而时间剪辑改的是局部时钟,只影响被包住的子图。这正是文档把两种方式并列但语义不同的原因。
四、手段二:时间轴上的 Time Clip(时间剪辑)
文档指出"时间轴上的时间剪辑同样做这件事:每个剪辑门控图中哪一部分会被求值,并重映射喂入其子图的时间"。下面从数据结构到求值流程逐层印证。
4.1TimeClip数据结构:两个区间的仿射映射
TimeClip.cs 定义了时间剪辑的核心数据:
TimeRange:时间轴上的摆放区间(文档注释明确"Timeline placement is always bars",摆放位置单位永远是 bar);SourceRange:源内容区间,即"这段剪辑对应素材的哪一段";LayerIndex:时间轴图层索引,重叠检测只在同图层内进行(TimeClip.cs);UsedForRegionMapping:标记该剪辑是否用于"嵌套内容局部时间重映射"。像TimeClipSwitch这类算子可以清掉这个标志,表示拖动剪辑时应把源区间与摆放区间联动(TimeClip.cs);SourceUnit(ClipTimeUnits枚举,Bars/Seconds):SourceRange所用单位。文档注释解释得很清楚——墙钟类素材(视频、音频)用Seconds,这样改 BPM 时裁剪点和关键帧仍"贴"在内容上;音乐类内容(嵌套合成、MIDI)用Bars(TimeClip.cs)。旧文件读取后由NeedsSourceUnitConversion标记一次性的单位换算。
时间重映射的数学核心是两个互逆的仿射函数:
// Core/Animation/TimeClip.cs#L85-L93 public double MapTimelineToSource(double timelineBars) { var pos = timelineBars - TimeRange.Start; var duration = TimeRange.End - TimeRange.Start; if (Math.Abs(duration) > 0.0001f) pos *= (SourceRange.End - SourceRange.Start) / duration; return pos + SourceRange.Start; }即source = (timeline - TimeRange.Start) * SourceRange.Duration / TimeRange.Duration + SourceRange.Start。文档强调这是"affine and unbounded — callers clamp or gate as needed"(仿射且无界,由调用方负责门控或钳制)。MapSourceToTimeline是其逆映射,用于把源时间上的数据(如关键帧)摆到它生效的全局播放位置上。
由此派生出文档中"红色提示 = 缩放"的判据:
// Core/Animation/TimeClip.cs#L75 public float Speed => MathF.Abs(TimeRange.Duration) < 0.001f ? 1 : SourceRange.Duration / TimeRange.Duration;Speed == 1且两区间重合时时间只是"平移"(remap);Speed != 1时内容被加速或减速(scaled);SourceRange反向时(负时长)则等价于倒放——文档所列 "run a scene backwards, hold it still, or jump non-linearly" 正是通过编辑这两个区间来实现的。
此外还有两个健壮性机制:
MakeConform()(TimeClip.cs):把任何非有限值(NaN/Inf)的区间端点修复为合法默认值,新剪辑默认时长 4 个 bar(DefaultClipDuration);SourceToSeconds/SecondsToSource/GetPlaybackSpeed(TimeClip.cs):在Bars单位下按当前 BPM 换算真实秒数,供媒体类算子做解码与展示。
4.2 求值流程:门控 + 重映射 + 恢复
时间覆盖真正生效的地方是 TimeClipSlot.cs。TimeClipSlot<T>是带时间剪辑的输入槽,其UpdateWithTimeRangeCheck(TimeClipSlot.cs)完整对应文档描述的"门控 + 重映射"两步:
// 1. 门控:时间轴时间不在 TimeRange 内 → 子图不求值 if (!EvaluateOutsideRange && ((context.LocalTime < TimeClip.TimeRange.Start) || (context.LocalTime >= TimeClip.TimeRange.End))) { LastUpdateStatus = CoreSettings.Config.TimeClipSuspending ? UpdateStates.Suspended : UpdateStates.Active; return; } // 2. 重映射:把全局/局部时间换算到源时间,再求值子图 var prevTime = context.LocalTime; var prevFxTime = context.LocalFxTime; context.LocalTime = TimeClip.MapTimelineToSource(prevTime); context.LocalFxTime = TimeClip.MapTimelineToSource(prevFxTime); _baseUpdateAction(context); // 3. 恢复:出栈时还原,保证嵌套/兄弟子图互不污染 context.LocalTime = prevTime; context.LocalFxTime = prevFxTime;几个值得注意的实现细节:
- Suspended 状态:范围外默认只标记
Inactive;当全局设置CoreSettings.Config.TimeClipSuspending打开时,范围外的子图进入Suspended(挂起)状态以节省算力——这是"门控"的性能收益所在。 EvaluateOutsideRange旁路:媒体类剪辑(视频、音频)需要"首尾帧钳制/解码预卷",因此这类槽关掉范围外门控,改为总是求值、只做时间重映射,由算子自己负责钳制(TimeClipSlot.cs)。IContentTimeClip标记:SetOutputData中,若父算子实现IContentTimeClip(墙钟媒体剪辑,如 VideoClip / AudioClip),SourceUnit被设为Seconds,源时间轴锚定在素材 0 点而非时间轴摆放位置;否则为Bars(TimeClipSlot.cs)。- 旧工程兼容:若文件中读出的
TimeClip缺少SourceUnit字段(NeedsSourceUnitConversion为 true),槽会用最近一层拥有独立设置的合成的 BPM(FindCompositionBpm,逐级向上查找Symbol.CompositionSettings,取不到则回退Playback.Current?.Bpm ?? 120)把源区间与子图关键帧时间一次性换算成秒(TimeClipSlot.cs),并通过Animator.ScaleKeyTimesOfChild同步缩放关键帧——保证升级前的文件打开后时间语义不变。 - 嵌套安全:重映射前后对
context.LocalTime做保存/恢复,因此一个算子的时间覆盖只作用于自己的子树,父图与兄弟子图不受影响。
4.3 被覆盖的时间流向了哪里
局部时间一旦改变,下游所有"按局部时间采样"的机制都会跟着走。Animator.cs 的注释与实现表明:关键帧曲线在context.LocalTime处采样(例如GetSampledValue(ctx.LocalTime)),且键值必须插在局部时间空间中。因此一个 Time Clip 覆盖的不仅是一个算子的"感觉",而是子图内所有关键帧动画、所有读取LocalTime/LocalFxTime的算子的时间基准——这与文档"everything inside animates against that instead of the timeline"的表述精确对应。
五、时间轴上的橙色与红色提示
文档提到:时间轴上橙色(orange)与红色(red)的提示分别表示"时间正在被重映射"或"被缩放"。结合 TimeClip.cs 的Speed属性与 TimeClipItem.cs 中"time-remap is the norm for them"的注释,可以推断其呈现逻辑为:
- 重映射(remap):
SourceRange与TimeRange不重合,但时长比接近 1——内容只是被"搬"到了时间轴的另一个位置; - 缩放(scaled):
Speed = SourceRange.Duration / TimeRange.Duration偏离 1——内容在单位时间内的播放速度被改变(负值为倒放)。
从源码结构看,剪辑绘制时以SourceRange和TimeRange的对比结果作为着色依据;对媒体类内容剪辑(IContentTimeClip),由于"重映射是常态",UI 不再把重映射本身当作值得高亮的状态。
六、组合两种手段时的注意事项
- 渲染导出路径:
Playback在IsRenderingToFile为 true 时会把PlaybackSpeed置 0,并由渲染输出外部提供时间(Playback.cs)。此时SetPlaybackTime这类在帧间抢写全局时间的算子可能与渲染驱动的时间源竞争,导出场景应优先依赖 Time Clip 的静态重映射。 - 单位一致性:在 120 BPM 下 1 bar = 2 秒。
Time算子的Units=Secs、TimeClip的SourceUnit=Seconds、SourceToSeconds等 API 都是为消除 bar/秒混用而设;BPM 变化后,Bars单位的内容(嵌套合成、MIDI)会随拍走,Seconds单位的内容(视频、音频)保持与墙钟一致。 - 倒放/冻结的实现选择:文档列举的 "run a scene backwards, hold it still, or jump non-linearly through an animation" 可分别落到:
SourceRange反向或时间信号递减(倒放);Time算子Frozen模式(冻结,且自动停掉动画失效传播);正弦/随机/节拍信号驱动TimeInBars(非线性跳转)。 - 范围外行为:需要确认全局设置中
TimeClipSuspending是否启用,以决定范围外的子图是挂起(省性能)还是仅标记不活跃(状态可瞬时恢复)。
七、小结
t3 的时间覆盖机制可以归纳为一句话:全局Playback.TimeInBars是默认时钟,而时间覆盖就是在子图求值边界上替换context.LocalTime这一时钟源。它有两个落地形态:一是用信号驱动算子(Time算子读取多种时间源、SetPlaybackTime写入全局时钟);二是时间轴上的 Time Clip(TimeClip双区间仿射映射 +TimeClipSlot的门控/重映射/恢复三段式求值)。核心实现集中在 TimeClip.cs、TimeClipSlot.cs、Playback.cs、EvaluationContext.cs 与 SetPlaybackTime.cs 等文件,帮助文档见 TimeOverrides.md。掌握这套机制后,你可以独立地为任意子图搭建自己的时钟,实现倒放、冻结、循环与节拍驱动的动画重放。
【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考