mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-07 19:11:54 +00:00
* Cryptomatte: the Layer popup lists the footage's Cryptomatte layers (#473) The popup was the constant CryptoObject / CryptoMaterial / CryptoAsset. It now lists the Cryptomatte layers of the layer's OpenEXR footage, and those three when there are none. The stored value keeps its meaning (0-2 the standard layers, 3 and up the file's other layers in name order), so older projects render the same. Refs #473 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Cryptomatte Layer popup: timeline editor, one menu helper, probe and fallback tests (#473) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Cryptomatte Layer popup: give further layers of a standard kind a custom slot A.CryptoObject keeps slot 0; B.CryptoObject and later ones get 3+ instead of being dropped, so they can be selected, picked and rendered. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Format; note same-kind layers in the footage docs Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Format the merged code with rustfmt Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fdcc715829
commit
519769299f
11 files changed
+345
-20
No files matched your search
@@ -409,8 +409,62 @@ pub fn selection_hashes(sel: &str, manifest: Option<&[(String, u32)]>) -> Vec<u3
|
||||
out
|
||||
}
|
||||
|
||||
/// The Cryptomatte layers every renderer names, and the Layer popup's options when the footage
|
||||
/// has no Cryptomatte metadata. The stored Layer value is an index: 0-2 are these (an exact
|
||||
/// match or a suffix such as `ViewLayer.CryptoObject`), 3 and up are the file's other Cryptomatte
|
||||
/// layers in name order, so a project saved before the popup followed the footage loads unchanged.
|
||||
pub const CRYPTO_LAYERS: [&str; 3] = ["CryptoObject", "CryptoMaterial", "CryptoAsset"];
|
||||
|
||||
/// The Layer popup's entries for Cryptomatte `layers` (sorted, as [`AuxChannels::crypto_layers`]
|
||||
/// returns them): the stored value and the layer's name, in value order. Of several layers
|
||||
/// that fit one standard slot (`A.CryptoObject`, `B.CryptoObject`) the first one keeps the standard
|
||||
/// slot, so existing projects load unchanged, and the others get the next custom slots.
|
||||
fn crypto_layer_slots(layers: &[String]) -> Vec<(usize, &str)> {
|
||||
let mut out: Vec<(usize, &str)> = vec![];
|
||||
let mut custom = CRYPTO_LAYERS.len();
|
||||
for l in layers {
|
||||
match CRYPTO_LAYERS.iter().position(|w| l == w || l.ends_with(w)) {
|
||||
Some(i) if !out.iter().any(|(j, _)| *j == i) => out.push((i, l)),
|
||||
_ => {
|
||||
out.push((custom, l));
|
||||
custom += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|(i, _)| *i);
|
||||
out
|
||||
}
|
||||
|
||||
/// What the Layer popup lists for the footage `aux`: the Cryptomatte layers the file has, as
|
||||
/// (stored value, name), or the three standard ones when there is none (no footage, not an
|
||||
/// OpenEXR, no Cryptomatte channels).
|
||||
pub fn crypto_layer_choices(aux: Option<&AuxChannels>) -> Vec<(usize, String)> {
|
||||
crypto_layer_menu(aux, 0).0
|
||||
}
|
||||
|
||||
/// The Layer popup for the stored value `index`: [`crypto_layer_choices`], and the name of the
|
||||
/// entry the effect reads (the first when `index` names none, as the effect does).
|
||||
pub fn crypto_layer_menu(aux: Option<&AuxChannels>, index: usize) -> (Vec<(usize, String)>, Option<String>) {
|
||||
let layers = aux.map(AuxChannels::crypto_layers).unwrap_or_default();
|
||||
if layers.is_empty() {
|
||||
let all: Vec<(usize, String)> = CRYPTO_LAYERS.iter().enumerate().map(|(i, n)| (i, n.to_string())).collect();
|
||||
let shown = all.iter().find(|c| c.0 == index).or(all.first()).map(|c| c.1.clone());
|
||||
return (all, shown);
|
||||
}
|
||||
let slots = crypto_layer_slots(&layers);
|
||||
let shown = slots.iter().find(|(i, _)| *i == index).map(|(_, n)| n.to_string()).or_else(|| layers.first().cloned());
|
||||
(slots.into_iter().map(|(i, n)| (i, n.to_string())).collect(), shown)
|
||||
}
|
||||
|
||||
/// The Cryptomatte layer of `aux` that the stored Layer value `index` reads: its slot's layer, else
|
||||
/// the first layer (what a file without that layer has always shown). `None` without any.
|
||||
pub fn crypto_layer_name(aux: &AuxChannels, index: usize) -> Option<String> {
|
||||
if aux.crypto_layers().is_empty() {
|
||||
return None;
|
||||
}
|
||||
crypto_layer_menu(Some(aux), index).1
|
||||
}
|
||||
|
||||
/// Rank planes as [`AuxChannels::crypto_rank_planes`] returns them.
|
||||
type RankPlanes<'a> = [(&'a [f32], &'a [f32])];
|
||||
|
||||
@@ -524,12 +578,7 @@ pub fn matte_only_px(m: f32, in_alpha: bool) -> Px {
|
||||
|
||||
fn cryptomatte(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let Some(a) = aux(ctx) else { return b };
|
||||
let layers = a.crypto_layers();
|
||||
if layers.is_empty() {
|
||||
return b;
|
||||
}
|
||||
let want = CRYPTO_LAYERS[(ctx.params.e("layer") as usize).min(2)];
|
||||
let layer = layers.iter().find(|l| l.as_str() == want || l.ends_with(want)).unwrap_or(&layers[0]).clone();
|
||||
let Some(layer) = crypto_layer_name(&a, ctx.params.e("layer") as usize) else { return b };
|
||||
let manifest = a.manifests.iter().find(|(l, _)| *l == layer).map(|(_, m)| m.as_slice());
|
||||
let sel = selection_hashes(ctx.params.s("selection"), manifest);
|
||||
let idx = index_map(&b, &a);
|
||||
|
||||
@@ -443,6 +443,114 @@ fn instance3d_replaces_the_pixels_with_the_referenced_layer() {
|
||||
assert_eq!(out.img.data, solid().data);
|
||||
}
|
||||
|
||||
/// A 20×10 layer of one Cryptomatte layer `name` that holds "left" on its left half and
|
||||
/// "right" on its right half, as [`scene`] does for `CryptoObject`.
|
||||
fn crypto_only(names: &[&str]) -> AuxHost {
|
||||
let (w, h) = (20usize, 10usize);
|
||||
let mut a = AuxChannels::new(w as u32, h as u32, 1.0);
|
||||
let (hl, hr) = (crypto_hash("left"), crypto_hash("right"));
|
||||
for name in names {
|
||||
let ids: Vec<f32> = (0..w * h).map(|i| crypto_float(if i % w < 10 { hl } else { hr })).collect();
|
||||
a.channels.push((format!("{name}00.R"), ids));
|
||||
a.channels.push((format!("{name}00.G"), vec![1.0; w * h]));
|
||||
a.manifests.push((name.to_string(), vec![("left".into(), hl), ("right".into(), hr)]));
|
||||
}
|
||||
AuxHost(Arc::new(a))
|
||||
}
|
||||
|
||||
fn choices(names: &[&str]) -> Vec<(usize, String)> {
|
||||
crypto_layer_choices(Some(&crypto_only(names).0))
|
||||
}
|
||||
|
||||
/// #473: the Layer popup lists the Cryptomatte layers the footage has, and the three standard
|
||||
/// ones when there is no footage, no OpenEXR or no Cryptomatte in it.
|
||||
#[test]
|
||||
fn crypto_layer_popup_lists_the_layers_of_the_footage() {
|
||||
let standard: Vec<(usize, String)> = CRYPTO_LAYERS.iter().enumerate().map(|(i, n)| (i, n.to_string())).collect();
|
||||
assert_eq!(crypto_layer_choices(None), standard);
|
||||
assert_eq!(crypto_layer_choices(Some(&AuxChannels::new(1, 1, 1.0))), standard, "an EXR without Cryptomatte");
|
||||
assert_eq!(crypto_layer_choices(Some(&scene().0)), vec![(0, "CryptoObject".to_string())]);
|
||||
// Only CryptoMaterial: it, not CryptoObject, with its own stored value.
|
||||
assert_eq!(choices(&["CryptoMaterial"]), vec![(1, "CryptoMaterial".to_string())]);
|
||||
// Standard layers come in slot order whatever the names sort like; prefixed ones fill their slot.
|
||||
assert_eq!(choices(&["CryptoAsset", "ViewLayer.CryptoObject"]), vec![(0, "ViewLayer.CryptoObject".to_string()), (2, "CryptoAsset".to_string())]);
|
||||
// A layer under another name gets a value after the standard ones, in name order.
|
||||
assert_eq!(choices(&["Zed", "CryptoMaterial", "Alpha"]), vec![(1, "CryptoMaterial".to_string()), (3, "Alpha".to_string()), (4, "Zed".to_string())]);
|
||||
// A name that only ends in a standard one fills its slot, as the effect always matched it.
|
||||
assert_eq!(
|
||||
choices(&["MyCryptoObject", "ViewLayer.CryptoMaterial", "Other"]),
|
||||
vec![(0, "MyCryptoObject".to_string()), (1, "ViewLayer.CryptoMaterial".to_string()), (3, "Other".to_string())]
|
||||
);
|
||||
// The entry shown: the value's own, else the first layer; with no layers, the standard one.
|
||||
let shown = |names: &[&str], i| crypto_layer_menu(Some(&crypto_only(names).0), i).1;
|
||||
assert_eq!(
|
||||
[0, 1, 7].map(|i| shown(&["CryptoMaterial"], i)),
|
||||
[Some("CryptoMaterial".to_string()), Some("CryptoMaterial".to_string()), Some("CryptoMaterial".to_string())]
|
||||
);
|
||||
assert_eq!(crypto_layer_menu(None, 2).1.as_deref(), Some("CryptoAsset"));
|
||||
assert_eq!(crypto_layer_menu(None, 9).1.as_deref(), Some("CryptoObject"), "a custom value with no footage");
|
||||
// Two layers for one slot: the first keeps it, the next gets a custom value.
|
||||
assert_eq!(choices(&["A.CryptoObject", "B.CryptoObject"]), vec![(0, "A.CryptoObject".to_string()), (3, "B.CryptoObject".to_string())]);
|
||||
assert_eq!(choices(&["A.CryptoObject", "B.CryptoObject", "C.CryptoObject", "Mine"]).iter().map(|c| c.0).collect::<Vec<_>>(), [0, 3, 4, 5]);
|
||||
}
|
||||
|
||||
/// A project saved with Layer = CryptoObject / CryptoMaterial / CryptoAsset renders the same layer
|
||||
/// as before: the stored value's own layer, else the file's first; and the custom layers are read
|
||||
/// by their value.
|
||||
#[test]
|
||||
fn crypto_layer_values_keep_their_meaning() {
|
||||
let all = crypto_only(&["CryptoAsset", "CryptoMaterial", "CryptoObject", "Mine"]).0;
|
||||
let name = |i| crypto_layer_name(&all, i);
|
||||
assert_eq!([0, 1, 2, 3].map(name), ["CryptoObject", "CryptoMaterial", "CryptoAsset", "Mine"].map(|n| Some(n.to_string())));
|
||||
// A value the file has no layer for reads its first layer, as the effect always did.
|
||||
assert_eq!(name(4).as_deref(), Some("CryptoAsset"));
|
||||
let only = crypto_only(&["CryptoMaterial"]).0;
|
||||
assert_eq!(
|
||||
[0, 1, 2].map(|i| crypto_layer_name(&only, i)),
|
||||
[Some("CryptoMaterial".to_string()), Some("CryptoMaterial".to_string()), Some("CryptoMaterial".to_string())]
|
||||
);
|
||||
assert_eq!(crypto_layer_name(&AuxChannels::new(1, 1, 1.0), 0), None);
|
||||
}
|
||||
|
||||
/// The effect keys the layer the Layer value names: a Material-only file renders its matte
|
||||
/// whether Layer says Object (a saved project) or Material (what the popup now stores), and a
|
||||
/// custom layer by its own value.
|
||||
#[test]
|
||||
fn cryptomatte_renders_the_layer_the_value_names() {
|
||||
let sel = ("selection", Value::Str("left".into()));
|
||||
let matte = |layer: u32, host: &AuxHost| run("ec.3d.cryptomatte", &[("layer", Value::Enum(layer)), sel.clone(), ("display", Value::Enum(1))], host);
|
||||
let only = crypto_only(&["CryptoMaterial"]);
|
||||
let (old, new) = (matte(0, &only), matte(1, &only));
|
||||
assert_eq!(old.data, new.data);
|
||||
assert_eq!((new.get(2, 5)[3], new.get(12, 5)[3]), (1.0, 0.0));
|
||||
// Two layers: "left" is in both, under hashes the manifests share, so tell them apart by
|
||||
// which has it on the right half.
|
||||
let (hl, hr) = (crypto_hash("left"), crypto_hash("right"));
|
||||
let mut a = AuxChannels::new(20, 10, 1.0);
|
||||
for (name, flip) in [("CryptoObject", false), ("Mine", true)] {
|
||||
let ids: Vec<f32> = (0..200).map(|i| crypto_float(if (i % 20 < 10) != flip { hl } else { hr })).collect();
|
||||
a.channels.push((format!("{name}00.R"), ids));
|
||||
a.channels.push((format!("{name}00.G"), vec![1.0; 200]));
|
||||
}
|
||||
let two = AuxHost(Arc::new(a));
|
||||
let (object, mine) = (matte(0, &two), matte(3, &two));
|
||||
assert_eq!((object.get(2, 5)[3], object.get(12, 5)[3]), (1.0, 0.0));
|
||||
assert_eq!((mine.get(2, 5)[3], mine.get(12, 5)[3]), (0.0, 1.0), "Layer 3 is the custom layer");
|
||||
// A second view's layer of a standard kind is selectable too: B.CryptoObject is Layer 3.
|
||||
let mut a = AuxChannels::new(20, 10, 1.0);
|
||||
for (name, flip) in [("A.CryptoObject", false), ("B.CryptoObject", true)] {
|
||||
let ids: Vec<f32> = (0..200).map(|i| crypto_float(if (i % 20 < 10) != flip { hl } else { hr })).collect();
|
||||
a.channels.push((format!("{name}00.R"), ids));
|
||||
a.channels.push((format!("{name}00.G"), vec![1.0; 200]));
|
||||
}
|
||||
let views = AuxHost(Arc::new(a));
|
||||
assert_eq!(crypto_layer_name(&views.0, 0).as_deref(), Some("A.CryptoObject"));
|
||||
assert_eq!(crypto_layer_name(&views.0, 3).as_deref(), Some("B.CryptoObject"));
|
||||
let (first, second) = (matte(0, &views), matte(3, &views));
|
||||
assert_eq!((first.get(2, 5)[3], first.get(12, 5)[3]), (1.0, 0.0));
|
||||
assert_eq!((second.get(2, 5)[3], second.get(12, 5)[3]), (0.0, 1.0), "Layer 3 renders B");
|
||||
}
|
||||
|
||||
/// #474: [`reads_aux`] names exactly the 3D Channel effects that ask the host for channels.
|
||||
#[test]
|
||||
fn reads_aux_names_the_effects_that_read_channels() {
|
||||
|
||||
@@ -116,7 +116,8 @@ pub use time_fx::{PixelMotion, TimewarpPlan, TwStep, pmb_plan, posterized_time,
|
||||
pub use ocio::color_stabilizer_maps;
|
||||
// GPU effects, part B (3D Channel, Immersive Video, colour management, simulation render passes).
|
||||
pub use channel3d::{
|
||||
CRYPTO_LAYERS, CryptoAux, MATTE_ONLY_IN_ALPHA, aux_point, crypto_aux_image, crypto_aux_image_with, id_color, matte_only_px, reads_aux, selection_hashes,
|
||||
CRYPTO_LAYERS, CryptoAux, MATTE_ONLY_IN_ALPHA, aux_point, crypto_aux_image, crypto_aux_image_with, crypto_layer_choices, crypto_layer_menu,
|
||||
crypto_layer_name, id_color, matte_only_px, reads_aux, selection_hashes,
|
||||
};
|
||||
pub use ocio::{ColorOp, Straight, Tf, color_program};
|
||||
pub use sim::{Acc, Blob, Bubble, Post, Shape, Sprite, SpritePlan, Tint, bubble_list, bubble_sample_bounds, drizzle_drops, mercury_blobs, sprite_plan};
|
||||
|
||||
@@ -517,12 +517,11 @@ impl CryptoProbe {
|
||||
media_overrides: &[],
|
||||
};
|
||||
let params = effectcraft_effects::flatten_params(g, &mut |pr| ctx.value(layer, pr));
|
||||
let want = effectcraft_effects::CRYPTO_LAYERS.get(params.e("layer") as usize).copied().unwrap_or("CryptoAsset");
|
||||
let aux = r.layer_aux(&ctx, layer).ok_or(NO_CRYPTO)?;
|
||||
if !live() {
|
||||
return Ok(None);
|
||||
}
|
||||
let layer_name = aux.resolve_crypto_layer(want).ok_or(NO_CRYPTO)?;
|
||||
let layer_name = effectcraft_effects::crypto_layer_name(&aux, params.e("layer") as usize).ok_or(NO_CRYPTO)?;
|
||||
// The grid of the effect's input buffer (footage keeps native pixels from 3/4 scale
|
||||
// up, proxies have their own size, earlier effects can pad it), rendered and cached.
|
||||
if !live() {
|
||||
|
||||
@@ -42,6 +42,10 @@ fn aux_at(t: Tick) -> AuxChannels {
|
||||
aux.channels.push((format!("CryptoObject00.{c}"), planes[k].clone()));
|
||||
}
|
||||
aux.manifests = vec![("CryptoObject".into(), ["left", "right", "late", COMMA, "star*"].iter().map(|n| (n.to_string(), crypto_hash(n))).collect())];
|
||||
// A second view's layer of the same kind (`Z.CryptoObject`, stored Layer 3): "right" everywhere.
|
||||
aux.channels.push(("Z.CryptoObject00.R".into(), vec![crypto_float(crypto_hash("right")); w * h]));
|
||||
aux.channels.push(("Z.CryptoObject00.G".into(), vec![1.0; w * h]));
|
||||
aux.manifests.push(("Z.CryptoObject".into(), vec![("right".into(), crypto_hash("right"))]));
|
||||
aux
|
||||
}
|
||||
|
||||
@@ -130,6 +134,17 @@ fn picks_replace_add_and_remove_in_one_undo_step_each() {
|
||||
assert_eq!((pick(&mut s, 2.0, "replace")["changed"].as_bool(), s.history.undo.len()), (Some(false), n));
|
||||
}
|
||||
|
||||
/// A second layer that fits a standard slot (`Z.CryptoObject` beside `CryptoObject`) is picked by
|
||||
/// its own Layer value (3); Layer 0 still reads the first one.
|
||||
#[test]
|
||||
fn pick_reads_the_second_layer_of_a_kind() {
|
||||
let mut s = session();
|
||||
assert_eq!(pick(&mut s, 1.0, "replace")["name"], "left");
|
||||
s.execute_checked("prop.set", json!({"path": "effects/#1/layer", "value": 3})).unwrap();
|
||||
assert_eq!(pick(&mut s, 1.0, "replace")["name"], "right");
|
||||
assert_eq!(s.execute("effect.cryptomatteAt", json!({"effect": 1, "x": 1.0, "y": 1.5})).unwrap()["name"], "right");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_points_layers_and_modes_are_errors_and_change_nothing() {
|
||||
let mut s = session();
|
||||
@@ -896,3 +911,42 @@ fn highlight_cost_with_six_ranks() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 8 × 4 footage whose Cryptomatte layers are `Mine` (`left` / `right` halves) and `CryptoMaterial`
|
||||
/// (the other way round).
|
||||
struct Layers;
|
||||
impl FootageSource for Layers {
|
||||
fn frame(&self, _: ItemId, f: &Footage, _: Tick) -> Option<Arc<Image>> {
|
||||
Some(Arc::new(Image::new(f.width, f.height)))
|
||||
}
|
||||
fn aux(&self, _: ItemId, _: &Footage, _: Tick) -> Option<Arc<AuxChannels>> {
|
||||
let mut aux = AuxChannels::new(8, 4, 1.0);
|
||||
for (name, flip) in [("Mine", false), ("CryptoMaterial", true)] {
|
||||
let ids = (0..32).map(|i| crypto_float(crypto_hash(if (i % 8 < 4) != flip { "left" } else { "right" }))).collect();
|
||||
aux.channels.push((format!("{name}00.R"), ids));
|
||||
aux.channels.push((format!("{name}00.G"), vec![1.0; 32]));
|
||||
}
|
||||
Some(Arc::new(aux))
|
||||
}
|
||||
}
|
||||
|
||||
/// #473: the probe reads the layer the Layer value names, as the effect does: 0 (a saved
|
||||
/// CryptoObject) on a file without it reads the file's first layer, 1 is CryptoMaterial, and 3
|
||||
/// is the file's other layer.
|
||||
#[test]
|
||||
fn pick_reads_the_layer_the_layer_value_names() {
|
||||
let mut s = Session { footage: Arc::new(Layers), importer: Some(Arc::new(Probe)), ..Default::default() };
|
||||
let item = s.execute("file.import", json!({"paths": ["/renders/layers.exr"]})).unwrap()["items"][0].clone();
|
||||
s.execute("comp.new", json!({"name": "C", "width": 8, "height": 4, "frameRate": 24, "duration": 2})).unwrap();
|
||||
s.execute("layer.addItem", json!({"item": item})).unwrap();
|
||||
s.execute_checked("effect.apply", json!({"effect": "Cryptomatte"})).unwrap();
|
||||
let name_at = |s: &mut Session, layer: u32| {
|
||||
s.execute_checked("prop.set", json!({"path": "effects/#1/layer", "value": layer})).unwrap();
|
||||
let r = s.execute("effect.cryptomatteAt", json!({"effect": 1, "x": 0.5, "y": 1.5})).unwrap();
|
||||
(r["name"].clone(), r["layer"].clone())
|
||||
};
|
||||
// The files' layers sort as CryptoMaterial, Mine (no manifest here, so the IDs have no name).
|
||||
assert_eq!(name_at(&mut s, 0).1, "CryptoMaterial", "a saved CryptoObject: the first layer");
|
||||
assert_eq!(name_at(&mut s, 1).1, "CryptoMaterial");
|
||||
assert_eq!(name_at(&mut s, 3).1, "Mine");
|
||||
}
|
||||
@@ -268,12 +268,7 @@ fn identifier(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<
|
||||
|
||||
fn cryptomatte(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let layers = a.crypto_layers();
|
||||
if layers.is_empty() {
|
||||
return Some(b);
|
||||
}
|
||||
let want = effectcraft_effects::CRYPTO_LAYERS[(pr.e("layer") as usize).min(2)];
|
||||
let layer = layers.iter().find(|l| l.as_str() == want || l.ends_with(want)).unwrap_or(&layers[0]).clone();
|
||||
let Some(layer) = effectcraft_effects::crypto_layer_name(a, pr.e("layer") as usize) else { return Some(b) };
|
||||
let manifest = a.manifests.iter().find(|(l, _)| *l == layer).map(|(_, m)| m.as_slice());
|
||||
let sel = effectcraft_effects::selection_hashes(pr.s("selection"), manifest);
|
||||
// Per aux pixel: coverage-weighted ID colours (RGB) and the selection's coverage (A), on all
|
||||
|
||||
@@ -219,6 +219,8 @@ fn prop_row(
|
||||
Value::Vec2(_) | Value::Vec3(_) => dims = value_vector(app, ui, &cx, &value, actions),
|
||||
Value::Color(_) => value_color(app, ui, &cx, &value, actions),
|
||||
Value::Bool(_) => value_toggle(app, ui, &cx, &value, actions),
|
||||
// Cryptomatte's Layer: the Cryptomatte layers the footage's metadata names.
|
||||
Value::Enum(_) if crate::panels::fx_editors::is_crypto_layer(group, prop) => value_crypto_layer(app, ui, &cx, &value, actions),
|
||||
Value::Enum(_) => value_popup(app, ui, &cx, &value, actions),
|
||||
Value::Layer(_) => value_layer_ref(app, ui, &cx, &value, actions),
|
||||
// EXtractoR's red / green / blue / alpha: a popup of the footage's OpenEXR channels (#295).
|
||||
@@ -407,6 +409,33 @@ fn value_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value:
|
||||
}
|
||||
}
|
||||
|
||||
/// Cryptomatte's Layer popup: the Cryptomatte layers of the layer's OpenEXR footage (the three
|
||||
/// standard ones when there are none). The stored value stays the index
|
||||
/// [`effectcraft_engine::effects::crypto_layer_name`] reads, so older projects load unchanged.
|
||||
fn value_crypto_layer(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value: &Value, actions: &mut Actions) {
|
||||
let Value::Enum(i) = value else {
|
||||
return;
|
||||
};
|
||||
let PropCx { t, uid, cy, vx, r, p, layer, prop, .. } = *cx;
|
||||
// The channels of the frame the render just read: decoded files are cached.
|
||||
let aux = app.session.layer_channels(layer.id);
|
||||
let (choices, shown) = effectcraft_engine::effects::crypto_layer_menu(aux.as_deref(), *i as usize);
|
||||
let names: Vec<String> = choices.iter().map(|c| c.1.clone()).collect();
|
||||
let widest = names.iter().map(|o| p.layout_no_wrap(o.clone(), Tokens::ui(11.5), t.text).size().x).fold(0.0, f32::max);
|
||||
let dr = Rect::from_min_size(pos2(vx, cy - 9.0), vec2((widest + 30.0).clamp(80.0, (r.max.x - vx - 56.0).max(80.0)), 18.0));
|
||||
let pop = egui::Id::unique(("ec-epop", uid));
|
||||
if widgets::dropdown(ui, dr, shown.as_deref().unwrap_or(""), &t, egui::Id::unique(("ec-e", uid))).clicked() {
|
||||
widgets::open_popup(ui, pop);
|
||||
}
|
||||
app.auto.add(&format!("effectControls.prop.{uid}.value"), dr, &prop.name);
|
||||
let cur = names.iter().position(|n| Some(n) == shown.as_ref());
|
||||
if let Some(ni) = widgets::popup_menu(ui, pop, dr.left_bottom(), &names, cur)
|
||||
&& let Some(c) = choices.get(ni)
|
||||
{
|
||||
cx.set_step(actions, json!(c.0));
|
||||
}
|
||||
}
|
||||
|
||||
fn value_layer_ref(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value: &Value, actions: &mut Actions) {
|
||||
let Value::Layer(l) = value else {
|
||||
return;
|
||||
|
||||
@@ -39,6 +39,7 @@ pub const COLORAMA: &str = "ec.color.colorama";
|
||||
pub const GLOW: &str = "ec.stylize.glow";
|
||||
pub const RESHAPE: &str = "ec.distort.reshape";
|
||||
pub const EXTRACTOR: &str = "ec.3d.extractor";
|
||||
pub const CRYPTOMATTE: &str = "ec.3d.cryptomatte";
|
||||
pub const MESH_WARP: &str = "ec.distort.meshwarp";
|
||||
|
||||
fn str_value(layer: &Layer, ectx: &EvalCtx, g: &PropGroup, id: &str) -> String {
|
||||
@@ -838,6 +839,17 @@ pub fn is_extractor_channel(g: &PropGroup, prop: &effectcraft_engine::project::P
|
||||
matches!(&g.kind, GroupKind::Effect { effect } if effect == EXTRACTOR) && EXTRACTOR_CHANNELS.contains(&prop.match_id.as_str())
|
||||
}
|
||||
|
||||
/// Whether `prop` (in group `g`) is the Cryptomatte effect's Layer.
|
||||
pub fn is_crypto_layer(g: &PropGroup, prop: &effectcraft_engine::project::Property) -> bool {
|
||||
matches!(&g.kind, GroupKind::Effect { effect } if effect == CRYPTOMATTE) && prop.match_id == "layer"
|
||||
}
|
||||
|
||||
/// Whether `prop` is the Layer of a Cryptomatte effect on `layer` (the timeline finds it without
|
||||
/// its group).
|
||||
pub fn is_crypto_layer_of(layer: &Layer, prop: &effectcraft_engine::project::Property) -> bool {
|
||||
layer.effects().is_some_and(|fx| fx.groups().any(|g| g.match_id == CRYPTOMATTE && g.get("layer").is_some_and(|q| q.uid == prop.uid)))
|
||||
}
|
||||
|
||||
/// The OpenEXR layer EXtractoR's red, green and blue come from (`""`: the unnamed layer), or
|
||||
/// `None` when they mix layers.
|
||||
fn extractor_layer(vals: &[String; 4]) -> Option<&str> {
|
||||
|
||||
@@ -5895,16 +5895,30 @@ fn enum_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>,
|
||||
let (p, layer, prop, at, uid) = (ve.p, ve.layer, ve.prop, ve.at, ve.uid);
|
||||
let t = app.tokens;
|
||||
let x = at.x;
|
||||
if let Some(options) = layer.popup_options(prop) {
|
||||
// Cryptomatte's Layer lists the footage's Cryptomatte layers (value k is the choice's own).
|
||||
let crypto = crate::panels::fx_editors::is_crypto_layer_of(layer, prop).then(|| {
|
||||
let aux = app.session.layer_channels(layer.id);
|
||||
effectcraft_engine::effects::crypto_layer_menu(aux.as_deref(), *i as usize)
|
||||
});
|
||||
let options = match &crypto {
|
||||
Some((choices, _)) => Some(std::borrow::Cow::Owned(choices.iter().map(|c| c.1.clone()).collect::<Vec<_>>())),
|
||||
None => layer.popup_options(prop),
|
||||
};
|
||||
if let Some(options) = options {
|
||||
let r = Rect::from_min_size(pos2(x, at.y - 9.0), vec2(150.0, 18.0));
|
||||
let label = options.get(*i as usize).cloned().unwrap_or_default();
|
||||
let label = match &crypto {
|
||||
Some((_, shown)) => shown.clone().unwrap_or_default(),
|
||||
None => options.get(*i as usize).cloned().unwrap_or_default(),
|
||||
};
|
||||
let cur = if crypto.is_some() { options.iter().position(|o| *o == label) } else { Some(*i as usize) };
|
||||
let pop = egui::Id::unique(("epop", uid));
|
||||
if widgets::dropdown(ui, r, &label, &t, egui::Id::unique(("en", uid))).clicked() {
|
||||
widgets::open_popup(ui, pop);
|
||||
}
|
||||
app.auto.add(&format!("timeline.prop.{uid}.value"), r, &prop.name);
|
||||
if let Some(ni) = widgets::popup_menu(ui, pop, r.left_bottom(), &options, Some(*i as usize)) {
|
||||
ve.set_step(actions, json!(ni));
|
||||
if let Some(ni) = widgets::popup_menu(ui, pop, r.left_bottom(), &options, cur) {
|
||||
let value = crypto.as_ref().and_then(|(choices, _)| choices.get(ni)).map_or(ni, |c| c.0);
|
||||
ve.set_step(actions, json!(value));
|
||||
}
|
||||
} else {
|
||||
p.text(pos2(x, at.y), Align2::LEFT_CENTER, fmt_num(*i as f64, 0), Tokens::ui(12.0), t.for_value(prop.has_expression()).hot_text);
|
||||
|
||||
@@ -555,6 +555,63 @@ fn extractor_offers_the_exr_layers_and_channels() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
/// #473: Cryptomatte's Layer popup lists the Cryptomatte layers of the footage's OpenEXR (a file
|
||||
/// with only CryptoMaterial shows CryptoMaterial, not CryptoObject), and the three standard ones
|
||||
/// when the layer has no such footage. The stored value stays the standard index.
|
||||
#[test]
|
||||
fn cryptomatte_layer_popup_follows_the_footage() {
|
||||
use exr::prelude::*;
|
||||
let (w, hh) = (8usize, 4usize);
|
||||
let ch = |n: &str, v: f32| AnyChannel::new(n, FlatSamples::F32(vec![v; w * hh]));
|
||||
let channels = AnyChannels::sort(vec![ch("R", 0.5), ch("G", 0.5), ch("B", 0.5), ch("CryptoMaterial00.R", 1.0), ch("CryptoMaterial00.G", 1.0)].into());
|
||||
let mut exr_layer = exr::prelude::Layer::new((w, hh), LayerAttributes::default(), Encoding::FAST_LOSSLESS, channels);
|
||||
exr_layer.attributes.other.insert(Text::from("cryptomatte/abc1234/name"), AttributeValue::Text(Text::from("CryptoMaterial")));
|
||||
exr_layer.attributes.other.insert(Text::from("cryptomatte/abc1234/manifest"), AttributeValue::Text(Text::from(r#"{"wood":"13851a76"}"#)));
|
||||
let path = std::env::temp_dir().join(format!("ec-ui-exr-crypto-{}.exr", std::process::id()));
|
||||
Image::from_layer(exr_layer).write().to_file(&path).unwrap();
|
||||
|
||||
let mut s = effectcraft_host::session();
|
||||
let item = s.execute("file.import", json!({"paths": [path.to_string_lossy()]})).unwrap()["items"][0].clone();
|
||||
s.execute("comp.new", json!({"name": "EXR", "width": 8, "height": 4, "frameRate": 24, "duration": 1})).unwrap();
|
||||
let l = s.execute("layer.addItem", json!({"item": item})).unwrap()["layer"].as_u64().unwrap();
|
||||
let solid = s.execute("layer.newSolid", json!({"name": "S", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let apply = |s: &mut Session, l: u64| s.execute("effect.apply", json!({"layers": [l], "effect": "Cryptomatte"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let (fx, solid_fx) = (apply(&mut s, l), apply(&mut s, solid));
|
||||
let layer_prop = |s: &Session, l: u64, fx: u64| {
|
||||
layer(s, l).effects().and_then(|f| f.groups().find(|g| g.uid == fx)).and_then(|g| g.get("layer")).map(|p| (p.uid, p.value.as_enum())).unwrap()
|
||||
};
|
||||
let (uid, stored) = layer_prop(&s, l, fx);
|
||||
assert_eq!(stored, 0, "a new effect stores CryptoObject");
|
||||
let (solid_uid, _) = layer_prop(&s, solid, solid_fx);
|
||||
s.state.selected_layers = vec![LayerId(l)];
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
app.show_panel(PanelKind::EffectControls);
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1700.0, 1100.0)).build_eframe(|_| app);
|
||||
settle(&mut h);
|
||||
// The file has only CryptoMaterial, so that is what the closed popup shows, and all it lists.
|
||||
assert!(h.query_by_label("CryptoMaterial").is_some() && h.query_by_label("CryptoObject").is_none());
|
||||
let at = rect_of(&h, &format!("effectControls.prop.{uid}.value")).center();
|
||||
click(&mut h, at);
|
||||
assert!(h.query_by_label("CryptoObject").is_none() && h.query_by_label("CryptoAsset").is_none(), "only the file's layers");
|
||||
assert_eq!(h.query_all_by_label("CryptoMaterial").count(), 2, "the popup and its entry");
|
||||
h.query_all_by_label("CryptoMaterial").last().unwrap().click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(layer_prop(&h.state().session, l, fx).1, 1, "stored as the standard CryptoMaterial value");
|
||||
|
||||
// A layer without Cryptomatte footage lists the standard three.
|
||||
h.state_mut().session.state.selected_layers = vec![LayerId(solid)];
|
||||
settle(&mut h);
|
||||
let at = rect_of(&h, &format!("effectControls.prop.{solid_uid}.value")).center();
|
||||
click(&mut h, at);
|
||||
for name in ["CryptoMaterial", "CryptoAsset"] {
|
||||
assert!(h.query_by_label(name).is_some(), "the standard layer {name}");
|
||||
}
|
||||
h.get_by_label("CryptoAsset").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(layer_prop(&h.state().session, solid, solid_fx).1, 2);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_picker_handle_follows_hover_then_commits_once_across_effects() {
|
||||
for (effect, parameter) in [("ec.control.point", "point"), ("ec.blur.ccradialfast", "center"), ("ec.generate.gradientramp", "start")] {
|
||||
|
||||
+8
-1
@@ -133,7 +133,14 @@ The Cryptomatte effect keys objects by their Cryptomatte ID passes, with the nam
|
||||
file's manifest. Pick them with the eyedropper next to its Selection, then click in the
|
||||
Composition viewer (Shift adds, Alt or Ctrl removes). Matte Only shows the matte as grey in
|
||||
RGB over full alpha, as After Effects' Cryptomatte plug-in does; projects saved before that
|
||||
change keep their matte in alpha as well.
|
||||
change keep their matte in alpha as well. Its Layer popup lists the Cryptomatte
|
||||
layers the footage has (a file with only `CryptoMaterial` shows just that), and the standard
|
||||
`CryptoObject`, `CryptoMaterial` and `CryptoAsset` when the layer has no Cryptomatte footage. The
|
||||
stored Layer value is unchanged for those three, so older projects load as they were while each kind appears once. With
|
||||
several layers of one kind (`A.CryptoObject`, `B.CryptoObject`) the first keeps the standard
|
||||
value; the others, like any other layers, are stored as 3 and up in the file's name order (so a value of 3 reads another layer in
|
||||
another file). The popup lists them in Effect Controls and the timeline; scripts and
|
||||
`prop.get` still see the three standard options.
|
||||
|
||||
## Replacing and reloading
|
||||
|
||||
|
||||
Reference in new issue
Block a user