Files
effectcraft/crates/host/tests/it/cryptomatte_pick.rs
T

128 lines
7.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The Cryptomatte eyedropper on real OpenEXR footage (#473): `effect.pickCryptomatte` reads the
//! file's Cryptomatte ranks and manifest, edits the Selection, and the rendered matte follows,
//! including undo.
use effectcraft_engine::Session;
use effectcraft_engine::time::Tick;
use effectcraft_raster::channels3d::{crypto_float, crypto_hash};
use serde_json::json;
const RIGHT: &str = "Right, \"Box\"";
/// An 8 × 4 EXR: white RGBA, and a CryptoObject layer with `Left` in columns before `split` and
/// the comma-and-quotes name from there, both named in its manifest.
fn crypto_exr(tag: &str, split: usize) -> String {
use exr::prelude::*;
let (w, h) = (8usize, 4usize);
let ids: Vec<f32> = (0..w * h).map(|i| crypto_float(crypto_hash(if i % w < split { "Left" } else { RIGHT }))).collect();
let ch = |n: &str, v: Vec<f32>| AnyChannel::new(n, FlatSamples::F32(v));
let one = || vec![1.0f32; w * h];
let channels = AnyChannels::sort(
vec![ch("R", one()), ch("G", one()), ch("B", one()), ch("A", one()), ch("CryptoObject00.R", ids), ch("CryptoObject00.G", one())].into(),
);
let mut layer = Layer::new((w, h), LayerAttributes::default(), Encoding::FAST_LOSSLESS, channels);
let manifest = json!({"Left": format!("{:08x}", crypto_hash("Left")), RIGHT: format!("{:08x}", crypto_hash(RIGHT))}).to_string();
for (k, v) in [("name", "CryptoObject".to_string()), ("manifest", manifest)] {
layer.attributes.other.insert(Text::from(format!("cryptomatte/f834d0a/{k}").as_str()), AttributeValue::Text(Text::from(v.as_str())));
}
// A folder of its own: numbered siblings would import as an image sequence.
let dir = std::env::temp_dir().join(format!("ec-crypto-pick-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("crypto.exr");
Image::from_layer(layer).write().to_file(&path).unwrap();
path.to_string_lossy().to_string()
}
/// A session with the EXR as a layer of a 2 s comp, under a Cryptomatte effect.
fn footage(tag: &str, split: usize) -> (Session, String) {
let path = crypto_exr(tag, split);
let mut s = effectcraft_host::session();
let r = s.execute("file.import", json!({"paths": [path]})).unwrap();
assert_eq!(r["errors"], json!([]), "{r}");
s.execute("comp.new", json!({"name": "Crypto", "width": 8, "height": 4, "frameRate": 24, "duration": 2})).unwrap();
s.execute("layer.addItem", json!({"item": r["items"][0]})).unwrap();
s.execute_checked("effect.apply", json!({"effect": "Cryptomatte"})).unwrap();
(s, path)
}
/// The matte's alpha over the left and right halves.
fn matte(s: &Session) -> [f32; 2] {
let img = s.render(s.active_comp_id().unwrap(), Tick::ZERO, Default::default());
[img.get(1, 1)[3], img.get(6, 2)[3]]
}
#[test]
fn picking_on_a_cryptomatte_exr_edits_the_selection_and_the_matte() {
let (mut s, path) = footage("edit", 4);
s.execute_checked("prop.set", json!({"path": "effects/#1/display", "value": 1})).unwrap();
assert_eq!(matte(&s), [0.0, 0.0], "nothing selected");
let pick = |s: &mut Session, x: f64, mode: &str| s.execute_checked("effect.pickCryptomatte", json!({"effect": 1, "x": x, "y": 1, "mode": mode})).unwrap();
assert_eq!(pick(&mut s, 1.0, "replace")["selection"], "Left");
assert_eq!(matte(&s), [1.0, 0.0]);
let r = pick(&mut s, 6.0, "add");
assert_eq!((r["name"].as_str(), r["layer"].as_str()), (Some(RIGHT), Some("CryptoObject")), "{r}");
assert_eq!(r["selection"], r#"Left, "Right, \"Box\"""#);
assert_eq!(matte(&s), [1.0, 1.0], "the quoted name selects the right half");
pick(&mut s, 2.0, "remove");
assert_eq!(matte(&s), [0.0, 1.0]);
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(matte(&s), [1.0, 1.0]);
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(matte(&s), [1.0, 0.0]);
let _ = std::fs::remove_dir_all(std::path::Path::new(&path).parent().unwrap());
}
/// At half resolution a preview pixel shows the aux texel at its centre (layer x 3 for preview
/// pixel 1): `scale` makes the pick read that one, so what is clicked is what lights up.
#[test]
fn a_pick_at_preview_resolution_reads_the_pixel_the_preview_shows() {
let (mut s, path) = footage("half", 3);
s.execute_checked("prop.set", json!({"path": "effects/#1/matteOnly", "value": true})).unwrap();
let r = s.execute_checked("effect.pickCryptomatte", json!({"effect": 1, "x": 2.5, "y": 1.5, "scale": 0.5})).unwrap();
assert_eq!(r["name"], RIGHT, "{r}");
let opts = effectcraft_engine::render::RenderOpts { scale: 0.5, ..Default::default() };
assert_eq!(s.render(s.active_comp_id().unwrap(), Tick::ZERO, opts).get(1, 0)[3], 1.0);
// At full resolution the same point is Left's pixel.
let r = s.execute_checked("effect.cryptomatteAt", json!({"effect": 1, "x": 2.5, "y": 1.5})).unwrap();
assert_eq!(r["name"], "Left");
let _ = std::fs::remove_dir_all(std::path::Path::new(&path).parent().unwrap());
}
/// A keyframed Selection picked at `time` is edited at that time, not at the current time.
#[test]
fn a_keyframed_selection_is_edited_where_it_was_read() {
let (mut s, path) = footage("key", 4);
for t in [0.0, 1.0] {
s.execute_checked("prop.addKey", json!({"path": "effects/#1/selection", "time": t, "value": format!("<{:08x}>", crypto_hash(RIGHT))})).unwrap();
}
let r = s.execute_checked("effect.pickCryptomatte", json!({"effect": 1, "x": 0.5, "y": 1.5, "mode": "add", "time": 1.0})).unwrap();
assert_eq!((r["changed"].as_bool(), r["selected"].as_bool()), (Some(true), Some(true)), "{r}");
let sel = |s: &mut Session, t: f64| {
let r = s.execute("effect.cryptomatteAt", json!({"effect": 1, "x": 0.5, "y": 1.5, "time": t})).unwrap();
r["selected"].as_bool()
};
assert_eq!((sel(&mut s, 0.0), sel(&mut s, 1.0)), (Some(false), Some(true)), "only the key at 1 s gains Left");
let _ = std::fs::remove_dir_all(std::path::Path::new(&path).parent().unwrap());
}
/// A precomp whose 3D layer an expression moves: the pick finds it where the render keys it
/// (the pick's aux pass runs the session's expressions).
#[test]
fn a_pick_on_a_precomp_follows_expressions() {
let mut s = effectcraft_host::session();
let inner = s.execute("comp.new", json!({"name": "Inner", "width": 40, "height": 20, "frameRate": 24, "duration": 1})).unwrap()["comp"].clone();
let l = s.execute("layer.newSolid", json!({"name": "Ball", "width": 6, "height": 6})).unwrap()["layer"].clone();
s.execute("layer.setSwitch", json!({"layers": [l], "switch": "threeD", "value": true})).unwrap();
s.execute_checked("prop.setExpression", json!({"layer": l, "path": "transform/position", "expression": "[33, 10, 0]"})).unwrap();
s.execute("comp.new", json!({"name": "Outer", "width": 40, "height": 20, "frameRate": 24, "duration": 1})).unwrap();
s.execute("layer.addItem", json!({"item": inner})).unwrap();
s.execute_checked("effect.apply", json!({"effect": "Cryptomatte"})).unwrap();
s.execute_checked("prop.set", json!({"path": "effects/#1/matteOnly", "value": true})).unwrap();
let r = s.execute_checked("effect.pickCryptomatte", json!({"effect": 1, "x": 33.5, "y": 10.5})).unwrap();
assert_eq!(r["selection"], "Ball", "{r}");
let img = s.render(s.active_comp_id().unwrap(), Tick::ZERO, Default::default());
assert_eq!((img.get(33, 10)[3], img.get(20, 10)[3]), (1.0, 0.0));
assert_eq!(s.execute("effect.cryptomatteAt", json!({"effect": 1, "x": 20.5, "y": 10.5})).unwrap(), serde_json::Value::Null);
}