Import: Interpret Footage ▸ Preserve RGB keeps the file's values (#411)

Cause: float footage (OpenEXR, float TIFF) was always sRGB-encoded on
decode, and Interpret Footage could not turn that off, so an OCIO effect
on the layer converted values that were already encoded.

Fix: After Effects' Interpret Footage ▸ Color ▸ Preserve RGB
(Footage::preserve_rgb; file.interpretFootage {"preserveRgb": true}; a
checkbox in the Interpret Footage form; Remember / Apply Interpretation
carry it). The decoder keeps float values linear, and the working-space
conversion from the colour profile and Interpret As Linear Light don't
apply, so the file's values enter the composition unconverted. The media
cache key includes the flag, so both interpretations decode separately.

Tests: pool::preserve_rgb_reads_float_exr_values_unconverted,
tests_color::preserve_rgb_footage_is_not_converted (managed, linearized
and not), tests_proxy::guess_alpha_invert_and_linear_light (command,
render, Apply Interpretation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-09 23:57:12 +07:00
co-authored by Claude Opus 5.5
parent 698ade94ac
commit d6de68b20d
13 changed files with 130 additions and 28 deletions
+9 -2
View File
@@ -553,6 +553,8 @@ pub(crate) struct Interpretation {
fields: Option<effectcraft_project::FieldOrder>,
profile: Option<Option<effectcraft_project::ColorSpace>>,
linear: Option<bool>,
/// Color ▸ Preserve RGB.
preserve_rgb: Option<bool>,
/// Start Timecode.
start: Option<StartTimecode>,
}
@@ -631,6 +633,7 @@ impl Interpretation {
None => None,
};
it.linear = super::b_p(p, "linearLight");
it.preserve_rgb = super::b_p(p, "preserveRgb");
// Start Timecode: Use Source File Timecode (`overrideStart: false`), or Override Start
// (timecode or a frame number).
let start_err = || bad(cmd, "startTimecode: timecode (0:00:41:20) or a frame number, 0 to 1e9");
@@ -705,6 +708,9 @@ impl Interpretation {
if let Some(v) = self.linear {
f.linear_light = v;
}
if let Some(v) = self.preserve_rgb {
f.preserve_rgb = v;
}
// After the frame rate: timecode is read at the footage's rate.
if let Some(start) = self.start.as_ref().and_then(|t| t.frame(f.frame_rate)) {
f.start_timecode = start;
@@ -785,7 +791,7 @@ fn interpret(s: &mut Session, p: &Value) -> Result<Value> {
Some(ItemKind::Footage(f)) => Some(json!({
"item": i.0, "alpha": format!("{:?}", f.alpha), "matteColor": f.premul_color, "invertAlpha": f.invert_alpha,
"fields": f.fields.label(), "pixelAspect": f.pixel_aspect, "loop": f.loop_count, "frameRate": f.frame_rate.as_f64(),
"linearLight": f.linear_light, "startTimecode": f.timecode(0), "overrideStart": f.start_timecode.is_some(),
"linearLight": f.linear_light, "preserveRgb": f.preserve_rgb, "startTimecode": f.timecode(0), "overrideStart": f.start_timecode.is_some(),
"frames": f.sequence_frames(),
})),
_ => None,
@@ -815,6 +821,7 @@ fn apply_interpretation(s: &mut Session, p: &Value) -> Result<Value> {
f.fields = src.fields;
f.invert_alpha = src.invert_alpha;
f.linear_light = src.linear_light;
f.preserve_rgb = src.preserve_rgb;
f.color_profile = src.color_profile;
if src.native_rate.is_some() {
if f.native_rate.is_none() {
@@ -1025,7 +1032,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Main...",
["File", "Interpret Footage"],
Some("Cmd+Alt+G"),
"{items?, frameRate?: fps|\"file\", alpha?: straight|premultiplied|ignore|guess, guessAlpha?, matteColor?, invertAlpha?, loop?, pixelAspect?, fields?: off|upper|lower, colorProfile?: srgb|rec709|rec2020|p3|auto, linearLight?, overrideStart?: bool (false: Use Source File Timecode), startTimecode?: timecode|frame (Override Start)}",
"{items?, frameRate?: fps|\"file\", alpha?: straight|premultiplied|ignore|guess, guessAlpha?, matteColor?, invertAlpha?, loop?, pixelAspect?, fields?: off|upper|lower, colorProfile?: srgb|rec709|rec2020|p3|auto, linearLight?, preserveRgb? (the file's values unconverted), overrideStart?: bool (false: Use Source File Timecode), startTimecode?: timecode|frame (Override Start)}",
has_footage_selection,
interpret
),
+1
View File
@@ -318,6 +318,7 @@ pub fn footage_metadata(f: &Footage) -> Value {
"alpha": format!("{:?}", f.alpha),
"colorProfile": f.color_profile.map(|c| format!("{c:?}")).unwrap_or_else(|| "sRGB (default)".into()),
"linearLight": f.linear_light,
"preserveRgb": f.preserve_rgb,
"fields": format!("{:?}", f.fields),
"sequenceFiles": f.sequence.len(),
"dataRows": f.data.as_ref().map(|d| d.lines().count().saturating_sub(1)),
+8
View File
@@ -272,4 +272,12 @@ fn guess_alpha_invert_and_linear_light() {
s.execute("file.rememberInterpretation", json!({"item": h.0})).unwrap();
s.execute("file.applyInterpretation", json!({"items": [w.0]})).unwrap();
assert!(foot(&s, w).linear_light);
// Color ▸ Preserve RGB (#411) is set, rendered and carried by Apply Interpretation.
let out = s.execute("file.interpretFootage", json!({"item": h.0, "preserveRgb": true})).unwrap();
assert_eq!(out["items"][0]["preserveRgb"], true);
let p = px(&s, c, 0.0, RenderOpts::default(), 1, 1);
assert!((p[0] - 0.25).abs() < 0.01, "white at 25 %, unconverted: {p:?}");
s.execute("file.rememberInterpretation", json!({"item": h.0})).unwrap();
s.execute("file.applyInterpretation", json!({"items": [w.0]})).unwrap();
assert!(foot(&s, w).preserve_rgb);
}
+19 -6
View File
@@ -6,7 +6,7 @@
//! Values stay gamma-encoded: like After Effects, footage pixels enter the working space as they
//! are encoded in the file. Alpha follows the footage's [`AlphaMode`] interpretation.
use effectcraft_project::AlphaMode;
use effectcraft_project::{AlphaMode, Footage};
use effectcraft_raster::{Image, Px};
use filmcraft_color::{Matrix, Range};
use filmcraft_frame::{Chroma, PixelData, VideoFrame};
@@ -17,11 +17,24 @@ use rayon::prelude::*;
pub(crate) struct AlphaOp {
mode: AlphaMode,
matte: [f32; 3],
/// Interpret Footage ▸ Preserve RGB: float values stay as the file stores them (not
/// sRGB-encoded).
preserve_rgb: bool,
}
impl AlphaOp {
pub(crate) fn new(mode: AlphaMode, matte: [f32; 3]) -> Self {
Self { mode, matte }
Self { mode, matte, preserve_rgb: false }
}
/// The interpretation of footage `f`.
pub(crate) fn of(f: &Footage) -> Self {
Self { preserve_rgb: f.preserve_rgb, ..Self::new(f.alpha, f.premul_color) }
}
/// A linear float value as the compositor takes it: sRGB-encoded, or the file's own value
/// under Preserve RGB.
#[inline(always)]
fn float(self, v: f32) -> f32 {
if self.preserve_rgb { v } else { encode(v) }
}
/// `c` is the file's colour (straight or premultiplied as the mode says), `a` its alpha.
#[inline(always)]
@@ -72,7 +85,7 @@ pub(crate) fn frame_to_image_in(f: &VideoFrame, op: AlphaOp, mut buf: Vec<Px>) -
for (o, s) in row.iter_mut().zip(src.as_chunks::<4>().0.iter()) {
let a = s[3];
let inv = if a > 0.0 { 1.0 / a } else { 0.0 };
let c = [encode(s[0] * inv) * a, encode(s[1] * inv) * a, encode(s[2] * inv) * a];
let c = [op.float(s[0] * inv) * a, op.float(s[1] * inv) * a, op.float(s[2] * inv) * a];
*o = if op.mode == AlphaMode::Ignore { [c[0], c[1], c[2], 1.0] } else { [c[0], c[1], c[2], a] };
}
});
@@ -231,7 +244,7 @@ fn convert_yuv<T: Sample>(img: &mut Image, w: usize, h: usize, chroma: Chroma, p
}
/// Convert a still decoded by the `image` crate. Float images (OpenEXR, float TIFF) hold linear
/// light and are sRGB-encoded here (values above 1 are kept).
/// light and are sRGB-encoded here (values above 1 are kept), unless the footage preserves RGB.
pub(crate) fn dynamic_to_image(img: &image::DynamicImage, op: AlphaOp) -> Image {
use image::DynamicImage as D;
let (w, h) = (img.width(), img.height());
@@ -251,9 +264,9 @@ pub(crate) fn dynamic_to_image(img: &image::DynamicImage, op: AlphaOp) -> Image
let a = s[3];
let c = if premul {
let inv = if a > 0.0 { 1.0 / a } else { 0.0 };
[encode(s[0] * inv) * a, encode(s[1] * inv) * a, encode(s[2] * inv) * a]
[op.float(s[0] * inv) * a, op.float(s[1] * inv) * a, op.float(s[2] * inv) * a]
} else {
[encode(s[0]), encode(s[1]), encode(s[2])]
[op.float(s[0]), op.float(s[1]), op.float(s[2])]
};
*o = op.apply(c, a.clamp(0.0, 1.0));
}
+30 -4
View File
@@ -37,7 +37,9 @@ fn colour_bars(w: u32, h: u32) -> Image {
struct Key {
path: Arc<str>,
frame: i64,
alpha: u8,
/// The alpha interpretation (bits 0–1) and Preserve RGB (bit 2): they change the decoded
/// pixels.
interp: u8,
matte: [u32; 3],
}
@@ -516,8 +518,9 @@ impl Inner {
AlphaMode::Premultiplied => 1,
AlphaMode::Ignore => 2,
};
let interp = alpha | (u8::from(footage.preserve_rgb) << 2);
let matte = if footage.alpha == AlphaMode::Premultiplied { footage.premul_color.map(f32::to_bits) } else { [0; 3] };
let key = |path: &str, frame| Key { path: path.into(), frame, alpha, matte };
let key = |path: &str, frame| Key { path: path.into(), frame, interp, matte };
match footage.kind {
FootageKind::Sequence if !footage.sequence.is_empty() => {
let i = Self::frame_index(footage.frame_rate, t, footage.sequence_frames(), footage.loop_count);
@@ -607,7 +610,7 @@ impl Inner {
fn prefetch(self: &Arc<Self>, _footage: &Footage, _t: Tick) {}
fn decode(&self, loc: &Loc, footage: &Footage) -> Result<Image> {
let op = AlphaOp::new(footage.alpha, footage.premul_color);
let op = AlphaOp::of(footage);
let path = &loc.key.path;
match loc.media_t {
None if &**path == PLACEHOLDER => Ok(colour_bars(footage.width, footage.height)),
@@ -759,7 +762,7 @@ mod tests {
fn lru_respects_budget_and_recycles() {
let mut c = Lru::default();
let img = || Arc::new(Image::new(16, 16)); // 4 KiB + overhead
let k = |i| Key { path: "a".into(), frame: i, alpha: 0, matte: [0; 3] };
let k = |i| Key { path: "a".into(), frame: i, interp: 0, matte: [0; 3] };
let budget = 3 * (4096 + 64);
for i in 0..10 {
c.insert(k(i), img(), budget);
@@ -774,4 +777,27 @@ mod tests {
assert_eq!(c.take_spare(256).len(), 256);
assert!(c.take_spare(17).is_empty());
}
/// #411: Interpret Footage ▸ Preserve RGB reads a float OpenEXR's values as the file stores
/// them (no automatic linear → sRGB encode), so an OCIO effect is the only conversion; the
/// default interpretation still encodes. Both interpretations of one file decode separately.
#[test]
fn preserve_rgb_reads_float_exr_values_unconverted() {
use exr::prelude::*;
let (w, h) = (2usize, 2usize);
let ch = |n: &str, v: f32| AnyChannel::new(n, FlatSamples::F32(vec![v; w * h]));
let channels = AnyChannels::sort(vec![ch("R", 0.18), ch("G", 0.5), ch("B", 2.0), ch("A", 1.0)].into());
let image = exr::image::Image::from_layer(Layer::new((w, h), LayerAttributes::default(), Encoding::FAST_LOSSLESS, channels));
let mut bytes = std::io::Cursor::new(Vec::new());
image.write().to_buffered(&mut bytes).unwrap();
let b: Arc<[u8]> = bytes.into_inner().into();
let pool = MediaPool::new();
pool.add_bytes("/render.exr", b.clone());
let f = crate::probe_bytes("/render.exr", b).unwrap();
let encoded = pool.frame_at(&f, Tick::ZERO).unwrap().get(0, 0);
assert!((encoded[0] - 0.4613).abs() < 1e-3 && (encoded[1] - 0.7354).abs() < 1e-3 && encoded[2] > 1.3, "sRGB-encoded: {encoded:?}");
let raw = pool.frame_at(&Footage { preserve_rgb: true, ..f.clone() }, Tick::ZERO).unwrap().get(0, 0);
assert_eq!(raw, [0.18, 0.5, 2.0, 1.0]);
assert_eq!(pool.frame_at(&f, Tick::ZERO).unwrap().get(0, 0), encoded, "the default interpretation is cached apart");
}
}
+11
View File
@@ -963,6 +963,11 @@ pub struct Footage {
/// Interpret Footage ▸ Color ▸ Interpret As Linear Light: the file's values are linear.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub linear_light: bool,
/// Interpret Footage ▸ Color ▸ Preserve RGB: the file's values enter the composition
/// unconverted. Float footage (OpenEXR, float TIFF) is not sRGB-encoded on decode, and the
/// colour profile, Interpret As Linear Light and the working space don't apply.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub preserve_rgb: bool,
/// Data footage ([`FootageKind::Data`]): the file's text (JSON, CSV or TSV), kept in the
/// project so expressions read it everywhere (and on the web).
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1009,6 +1014,7 @@ impl Default for Footage {
fields: FieldOrder::Off,
invert_alpha: false,
linear_light: false,
preserve_rgb: false,
data: None,
layer: None,
page: 0,
@@ -1019,6 +1025,11 @@ impl Default for Footage {
}
impl Footage {
/// Interpret As Linear Light applies (Preserve RGB passes the file's values through).
pub fn linear_light_applies(&self) -> bool {
self.linear_light && !self.preserve_rgb
}
/// Source time as the footage's timecode: its frame `frame` from Start Timecode
/// (`0:00:41:20`), as the Footage panel shows it.
pub fn timecode(&self, frame: i64) -> String {
+6 -5
View File
@@ -17,7 +17,7 @@
//! pixels to their parent). Without a working space the sRGB curve is used.
use effectcraft_color::{ColorSpace, Conversion};
use effectcraft_project::{HdrMode, ProjectSettings};
use effectcraft_project::{Footage, HdrMode, ProjectSettings};
use effectcraft_raster::Image;
use rayon::prelude::*;
@@ -57,10 +57,11 @@ impl Pipe {
self.space.unwrap_or(ColorSpace::Srgb)
}
/// Footage with colour profile `profile` (`None` = sRGB) → working space.
pub fn media_in(&self, profile: Option<ColorSpace>) -> Option<Conversion> {
let ws = self.space?;
Conversion::new(profile.unwrap_or(ColorSpace::Srgb), false, ws, self.linear)
/// Footage `f` from its colour profile (`None` = sRGB) → working space; nothing for
/// Preserve RGB, whose values enter the working space as the file stores them.
pub fn media_in(&self, f: &Footage) -> Option<Conversion> {
let ws = self.space.filter(|_| !f.preserve_rgb)?;
Conversion::new(f.color_profile.unwrap_or(ColorSpace::Srgb), false, ws, self.linear)
}
/// Authored colours (solids, text, shapes) → working space: linearised in a linear working
+6 -5
View File
@@ -1089,7 +1089,7 @@ impl<'a> Renderer<'a> {
let k = self.raster_scale(ctx, layer);
if let Some(img) = self.footage.vector_frame(*item, f, k) {
let mut buf = Buf { img: (*img).clone(), offset: [0.0; 2], scale: k };
if let Some(c) = self.pipe.media_in(f.color_profile) {
if let Some(c) = self.pipe.media_in(f) {
color::convert(&mut buf.img, &c);
}
return Some(buf);
@@ -1127,7 +1127,7 @@ impl<'a> Renderer<'a> {
} else {
Buf { img: img.clone(), offset: [0.0; 2], scale: 1.0 / k }
};
if let Some(c) = self.pipe.media_in(f.color_profile) {
if let Some(c) = self.pipe.media_in(f) {
color::convert(&mut buf.img, &c);
}
buf
@@ -1156,7 +1156,7 @@ impl<'a> Renderer<'a> {
return Some(Arc::new(interpret_pixels(&field_frame(&img, parity), f)));
}
let img = self.footage_frame_raw(ctx, layer, item, f, t)?;
if f.invert_alpha || f.linear_light { Some(Arc::new(interpret_pixels(&img, f))) } else { Some(img) }
if f.invert_alpha || f.linear_light_applies() { Some(Arc::new(interpret_pixels(&img, f))) } else { Some(img) }
}
fn footage_frame_raw(&self, ctx: &EvalCtx, layer: &Layer, item: ItemId, f: &Footage, t: Tick) -> Option<Arc<Image>> {
@@ -2024,7 +2024,8 @@ pub fn field_frame(img: &Image, parity: usize) -> Image {
/// Interpret Footage ▸ Invert Alpha and Interpret As Linear Light on decoded (premultiplied)
/// pixels.
pub fn interpret_pixels(img: &Image, f: &Footage) -> Image {
if !f.invert_alpha && !f.linear_light {
let linear = f.linear_light_applies();
if !f.invert_alpha && !linear {
return img.clone();
}
let space = f.color_profile.unwrap_or(effectcraft_color::ColorSpace::Srgb);
@@ -2032,7 +2033,7 @@ pub fn interpret_pixels(img: &Image, f: &Footage) -> Image {
for p in out.data.iter_mut() {
let a = p[3];
let mut c = if a > 1e-6 { [p[0] / a, p[1] / a, p[2] / a] } else { [0.0; 3] };
if f.linear_light {
if linear {
// The file's values are linear light: encode them like the rest of the footage.
c = c.map(|v| space.encode(v));
}
+28
View File
@@ -198,6 +198,34 @@ fn footage_profile_is_converted_into_the_working_space() {
assert!((o[0] - 1.0).abs() < 2e-3 && o[1].abs() < 2e-3 && o[2].abs() < 2e-3, "{o:?}");
}
/// #411: Interpret Footage ▸ Preserve RGB: the footage's values enter the composition as the file
/// stores them, whatever its colour profile, Interpret As Linear Light or the working space.
#[test]
fn preserve_rgb_footage_is_not_converted() {
let render = |linearize: bool, f: Footage| {
let (mut p, cid, comp) = setup(BitDepth::Bpc32);
p.settings.working_space = Some(ColorSpace::Rec2020);
p.settings.linearize = linearize;
let fid = p.add_item("F", Label::Aqua, None, ItemKind::Footage(f));
let l = build::layer(&mut p, &comp, "F", LayerSource::Footage { item: fid }, (40, 20), None);
p.comp_mut(cid).unwrap().layers.push(l);
let src = Flat([0.18, 0.5, 0.75]);
let out = Renderer::new(&p, &src, RenderOpts::default()).comp_frame(cid, Tick::ZERO).get(5, 5);
// Back from the output conversion (working space → sRGB) to the working space's values.
let ws = effectcraft_color::Conversion::new(ColorSpace::Srgb, false, ColorSpace::Rec2020, linearize).unwrap();
let c = ws.apply([out[0], out[1], out[2]]);
[c[0], c[1], c[2], out[3]]
};
let preserve = Footage { preserve_rgb: true, linear_light: true, ..footage(Some(ColorSpace::DisplayP3)) };
for linearize in [false, true] {
let o = render(linearize, preserve.clone());
assert!((o[0] - 0.18).abs() < 1e-3 && (o[1] - 0.5).abs() < 1e-3 && (o[2] - 0.75).abs() < 1e-3, "linearize {linearize}: {o:?}");
// Without Preserve RGB the profile and linear light convert the values.
let o = render(linearize, Footage { preserve_rgb: false, ..preserve.clone() });
assert!((o[0] - 0.18).abs() > 0.05, "linearize {linearize}: {o:?}");
}
}
#[test]
fn layer_cache_is_keyed_by_colour_settings() {
let (mut p, cid, _) = setup(BitDepth::Bpc8);
+1
View File
@@ -458,6 +458,7 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
],
f.color_profile.map_or(0, |c| 1 + effectcraft_engine::project::ColorSpace::ALL.iter().position(|x| *x == c).unwrap_or(0)),
),
Field::bool("preserveRgb", "Preserve RGB", f.preserve_rgb),
Field::bool("linearLight", "Interpret As Linear Light", f.linear_light),
];
// After Frame Rate: an image sequence's frames (missing ones show colour bars, as in
+3
View File
@@ -219,6 +219,9 @@ build real projects through these interfaces; they are worked examples of everyt
colour bars, as in After Effects. `file.interpretFootage {"items":[id], "frameRate":12,
"startTimecode":1001}` conforms them and sets Override Start (the item and layers that ran to its end get the new
length). See [footage.md](footage.md).
* **Colour of footage**: `file.interpretFootage {"items":[id], "preserveRgb":true}` is Interpret
Footage ▸ Preserve RGB: the file's values unconverted (float EXRs stay linear), for a
per-layer OCIO conversion or data passes.
* **Multi-layer OpenEXR**: the footage shows the beauty pass (an unnamed RGBA layer, else
Combined / Beauty / Image / RGBA…, never Cryptomatte or depth). `layer.channels {layer}`
lists a footage layer's EXR layers and
+7 -5
View File
@@ -62,22 +62,24 @@ used, and layers that ran to the end of the footage keep doing so.
| **Alpha** | *Straight - Unmatted*, *Premultiplied - Matted With Color* (with the matte colour), *Ignore* (opaque), or *Guess* (EffectCraft looks at the first frame). *Invert Alpha* flips it. |
| **Assume this frame rate** | The rate the frames play at. A 250-frame sequence at 25 fps lasts 10 seconds; at 24 fps, 10.4 seconds. |
| **Start Timecode** | *Use Source File Timecode* (0:00:00:00), or *Override Start*: the timecode the footage's first frame shows in the Footage panel. It relabels source time only; the frames stay the same. For a 1001–1250 render at 24 fps, Override Start 0:00:41:17 (frame 1001) makes source time count from the render's own frame numbers. |
| Fields, pixel aspect, loop, colour profile, linear light | As in After Effects. |
| Fields, pixel aspect, loop | As in After Effects. |
| **Assign Profile**, **Interpret As Linear Light** | The colour profile the file's values are in (the working space converts from it), and whether they are linear light. |
| **Preserve RGB** | The file's values enter the composition unconverted: no colour profile, linear light or working-space conversion, and float files (OpenEXR, float TIFF) keep their linear values instead of being sRGB-encoded. Use it to convert chosen layers yourself with an OCIO effect, or to read data passes (depth, mist, normals) as numbers. |
Settings ▸ Import ▸ **Report Missing Frames** (on by default) lists the missing frame numbers
when a sequence imports; Interpret Footage lists them too.
**File ▸ Interpret Footage ▸ Remember Interpretation / Apply Interpretation** copy the alpha,
fields, pixel aspect, loop and colour settings to other footage.
fields, pixel aspect, loop and colour settings (Preserve RGB too) to other footage.
Agents: `file.interpretFootage {"items": [id], "frameRate": 24, "alpha": "premultiplied",
"startTimecode": 1001}` (timecode or a frame number; `"overrideStart": false` goes back to Use
Source File Timecode).
"startTimecode": 1001, "preserveRgb": true}` (timecode or a frame number; `"overrideStart":
false` goes back to Use Source File Timecode).
## Multi-layer OpenEXR
Float OpenEXR renders hold linear light; EffectCraft sRGB-encodes them as they are read, like
After Effects with Interpret As Linear Light for 32 bpc.
After Effects with Interpret As Linear Light for 32 bpc, unless the item preserves RGB (above).
A multi-layer file shows its main RGBA channels, as in After Effects. Renders that keep every
pass in named layers (Blender's `ViewLayer.Combined`, its compositor's File Output `Image`,
Nuke's `beauty`) show the beauty pass: the layer named Combined, Beauty, Image, RGBA, RGB or
+1 -1
View File
@@ -172,7 +172,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
| 3D | 88% | 5.5 | reflections of other layers (Advanced 3D surfaces reflect only the Environment light; Material Options ▸ Appears in Reflections is stored but has no visible effect yet, #261), multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (280 run on the GPU since M13.29, EFF-5: M13.28 added the remaining pixel effects and the geometry and audio generators (Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads, Audio Spectrum / Waveform, Basic / Path Text); M13.29 added the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize, Curl Noise) and removed the fallbacks of Shatter's wireframe views, Foam's extras, Timewarp's Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs; M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; Fractal and the analysis / tool effects still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (G2 on 9 October: the viewer's resize cursors point the way each handle scales and turn with the layer, #393; Ctrl/Cmd-click on the Timeline's or the Composition panel's current-time display toggles Timecode / Frames, #370; Tab / Shift+Tab in a value being typed commits it and types in the next / previous value of the panel (Timeline, Effect Controls), #361; right-clicking a property's name in the Timeline opens its menu (Reset, Edit Value…, Separate Dimensions, Add / Remove Expression, Add Property to Essential Graphics), #407; the in-window menu bar works from the keyboard (Alt, arrows, Enter, Escape) and the app's shortcuts wait while a menu is open, #279; the Timeline's composition marker bin and numpad * add comp markers, #275; Settings ▸ Appearance ▸ UI Scale (75–200 %, beyond After Effects, which follows the system's scaling), #284; G2 on 7 October: Project items, files and effects dropped on the Composition viewer, Project items dropped in the Timeline between layers and at a time, #85, #88, #89; popup menus moved to fit the window take clicks, so Render Queue templates apply, #117; View ▸ New Viewer landed in M13.32; an automation id on every control of Composition and Solid Settings, Group / Ungroup Shapes in the Layer menu, Window ▸ Learn, the flowchart shortcuts and Reveal in Explorer landed in M3.10 and M3.13; the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
| Project | ≈ 68% | 5.5 | OCIO displays beyond the built-in tone map; relative footage paths and relinking when a project moves (unsaved-changes prompts, a modified mark that follows undo and project files that name their version and always reopen landed in M3.9 and M3.14; auto-save and crash recovery in M3.7; Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3; image sequence import options (Sequence, Force Alphabetical Order, frame rate, picked ranges, folders, the browser), missing-frame placeholders and Interpret Footage Start Timecode landed: #297) |
| Project | ≈ 68% | 5.5 | OCIO displays beyond the built-in tone map; relative footage paths and relinking when a project moves (unsaved-changes prompts, a modified mark that follows undo and project files that name their version and always reopen landed in M3.9 and M3.14; auto-save and crash recovery in M3.7; Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3; image sequence import options (Sequence, Force Alphabetical Order, frame rate, picked ranges, folders, the browser), missing-frame placeholders and Interpret Footage Start Timecode landed: #297; Interpret Footage ▸ Preserve RGB: #411) |
| Masks & roto | 74% | 5.0 | (audit estimates; Roto Brush 2.0 / 3.0's trained model, MobileSAM in the swappable `segment` module, has since landed in M13.35; variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
| Preview | 77% | 2.8 | GPU effects for the audio-driven effects and the stepped particle systems; content-keyed RAM preview frames (an edit drops every comp's), audio scrubbing, more snapshot slots (G2 on 7 October: Audio, Lock and Shy switches keep the cached frames and playback with audio waits for frames instead of skipping them, #103; running out of video memory renders the frame on the CPU and keeps fewer GPU frames instead of crashing, #106; RAM preview keys with the render options, least-recently-shown eviction, Auto resolution while playing, Cache Before Playback that fits, an achieved-fps readout, Cache Frames When Idle and purges that clear what they name landed in M4.5–M4.8; 3D channel, VR, OCIO, Card Wipe and the simulations' render passes run on the GPU since M13.23; Advanced 3D layers with blend modes / track mattes / Preserve Transparency and environment backgrounds composite on the GPU since M13.22; pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
| Tracking | ≈ 96% | 0.3 | face tracking's trained model (MediaPipe Face Landmarker, M13.36) is an optional download; without it the classical (skin model + feature components + shape model) fitter is weak on profile views and occluded faces; Rolling Shutter Ripple is approximated by Subspace Warp's mesh density (face tracking (Outline Only / Detailed Features with Face Track Points and Extract & Copy Face Measurements), Subspace Warp's content-preserving mesh warp on subspace-smoothed trajectories, and radial lens distortion (k1, k2) in the camera bundle adjustment with Undistort Footage landed in M13.3; Rolling Shutter Repair in M9.11; point tracker, mask tracking, Warp Stabilizer and the 3D Camera Tracker in M6.x / M12.5 / M12.6) |