mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
EXtractoR's Red, Green, Blue and Alpha are text parameters naming
channels, and Effect Controls drew nothing for text parameters: there
was no way to choose a layer or channel. And a multi-layer OpenEXR file
whose colour is only in named layers (`diffuse.R`, Blender's
`ViewLayer.Combined.R`) didn't import at all: the image decoder wants an
unnamed RGB layer ("does not contain non-deep rgb channels").
- Effect Controls: EXtractoR gets a Layer popup listing the footage's
OpenEXR layers. Picking one sets red, green, blue and alpha to its
channels in one undo step, as After Effects' EXtractoR dialog does (a
one-channel layer such as depth goes in all three colours). Red /
Green / Blue / Alpha are popups of the layer's own R, G, B, A, every
channel of the file, and (none). The lists are read only while open,
so playing an EXR sequence doesn't parse files on the UI thread.
- `layer.channels {layer}` lists a layer's EXR layers and channels and
what each layer shows in red, green, blue and alpha, for agents.
- Media: an EXR without an unnamed RGB layer imports and shows its
colour layer (one named combined, beauty or rgba first); its channels
stay available to EXtractoR and the 3D Channel effects.
- Raster: AuxChannels::layers / layer_rgba and split_channel.
Test: extractor_shows_the_chosen_layer_and_channels_of_a_layered_exr
(host: a layered EXR written with the exr crate imports, layer.channels,
EXtractoR renders spec and depth; it failed to import before),
extractor_offers_the_exr_layers_and_channels (UI: Layer popup, one undo
step, channel popup), layered_exr_without_an_unnamed_rgb_layer_imports
(media).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
68 lines
3.5 KiB
Rust
68 lines
3.5 KiB
Rust
//! Multi-layer OpenEXR footage and EXtractoR in a full session (#295): a file whose colour is
|
||
//! only in named layers (as Blender and Nuke write them) imports, `layer.channels` lists its
|
||
//! layers and channels, and EXtractoR shows the chosen ones.
|
||
|
||
use effectcraft_engine::Session;
|
||
use effectcraft_engine::time::Tick;
|
||
use serde_json::{Value, json};
|
||
|
||
/// An 8×4 EXR with layers `diffuse` (orange), `spec` (blue) and `depth` (Z = 0.25), and no
|
||
/// unnamed RGB layer.
|
||
fn layered_exr() -> String {
|
||
use exr::prelude::*;
|
||
let (w, h) = (8usize, 4usize);
|
||
let ch = |n: &str, v: f32| AnyChannel::new(n, FlatSamples::F32(vec![v; w * h]));
|
||
let channels = AnyChannels::sort(
|
||
vec![ch("diffuse.R", 1.0), ch("diffuse.G", 0.5), ch("diffuse.B", 0.0), ch("spec.R", 0.0), ch("spec.G", 0.0), ch("spec.B", 1.0), ch("depth.Z", 0.25)]
|
||
.into(),
|
||
);
|
||
let image = Image::from_layer(Layer::new((w, h), LayerAttributes::default(), Encoding::FAST_LOSSLESS, channels));
|
||
let path = std::env::temp_dir().join(format!("ec-exr-layers-{}.exr", std::process::id()));
|
||
image.write().to_file(&path).unwrap();
|
||
path.to_string_lossy().to_string()
|
||
}
|
||
|
||
fn px(s: &Session) -> [f32; 4] {
|
||
let cid = s.active_comp_id().unwrap();
|
||
s.render(cid, Tick::ZERO, Default::default()).get(4, 2)
|
||
}
|
||
|
||
fn near(a: [f32; 4], b: [f32; 3]) -> bool {
|
||
(0..3).all(|k| (a[k] - b[k]).abs() < 0.02)
|
||
}
|
||
|
||
#[test]
|
||
fn extractor_shows_the_chosen_layer_and_channels_of_a_layered_exr() {
|
||
let path = layered_exr();
|
||
let mut s = effectcraft_host::session();
|
||
let r = s.execute("file.import", json!({"paths": [path]})).unwrap();
|
||
assert_eq!(r["errors"], json!([]), "the file imports: {r}");
|
||
s.execute("comp.new", json!({"name": "EXR", "width": 8, "height": 4, "frameRate": 24, "duration": 1})).unwrap();
|
||
let layer = s.execute("layer.addItem", json!({"item": r["items"][0]})).unwrap()["layer"].clone();
|
||
// The picture is the colour layer (diffuse: orange).
|
||
assert!(near(px(&s), [1.0, 0.735, 0.0]), "{:?}", px(&s));
|
||
|
||
let ch = s.execute("layer.channels", json!({"layer": layer})).unwrap();
|
||
let names: Vec<&str> = ch["layers"].as_array().unwrap().iter().filter_map(|l| l["name"].as_str()).collect();
|
||
assert_eq!(names, ["depth", "diffuse", "spec"], "{ch}");
|
||
assert_eq!(ch["layers"][2]["rgba"], json!(["spec.R", "spec.G", "spec.B", ""]));
|
||
assert_eq!(ch["layers"][0]["rgba"], json!(["depth.Z", "depth.Z", "depth.Z", ""]), "one channel fills red, green and blue");
|
||
assert!(ch["channels"].as_array().unwrap().contains(&Value::from("depth.Z")));
|
||
|
||
// EXtractoR on the spec layer: blue.
|
||
s.execute("effect.apply", json!({"layers": [layer], "effect": "EXtractoR"})).unwrap();
|
||
for (param, v) in ["red", "green", "blue", "alpha"].iter().zip(["spec.R", "spec.G", "spec.B", ""]) {
|
||
s.execute("prop.set", json!({"layer": layer, "path": format!("effects/#1/{param}"), "value": v})).unwrap();
|
||
}
|
||
assert!(near(px(&s), [0.0, 0.0, 1.0]), "{:?}", px(&s));
|
||
// Depth in all three: grey at its value.
|
||
for param in ["red", "green", "blue"] {
|
||
s.execute("prop.set", json!({"layer": layer, "path": format!("effects/#1/{param}"), "value": "depth.Z"})).unwrap();
|
||
}
|
||
assert!(near(px(&s), [0.25, 0.25, 0.25]), "{:?}", px(&s));
|
||
// A layer that isn't OpenEXR has no channels.
|
||
let solid = s.execute("layer.newSolid", json!({"color": "#808080"})).unwrap()["layer"].clone();
|
||
assert_eq!(s.execute("layer.channels", json!({"layer": solid})).unwrap()["layers"], json!([]));
|
||
let _ = std::fs::remove_file(path);
|
||
}
|