Files
effectcraft/crates/render/src/tests_aux.rs
T
Brandon ThomasandClaude Opus 5.5 9a11c577f3 M9.11: 3D Channel effects on auxiliary channels
Aux-channel model (effectcraft_raster::channels3d: depth, object/material
ID, normals, UV, coverage, Cryptomatte ranks + manifests, MurmurHash3);
EffectHost::aux; multi-layer OpenEXR channels via the exr crate
(FootageSource::aux); the compositor's depth / layer-ID / Cryptomatte pass
(Renderer::comp_aux) so the effects work on precomps of 3D comps.
3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR,
Fog 3D, ID Matte, IDentifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:28:37 -04:00

99 lines
4.9 KiB
Rust

//! Auxiliary 3D channels: the comp's depth / ID / Cryptomatte pass and the 3D Channel effects on
//! a precomp of a 3D comp.
use effectcraft_color::Label;
use effectcraft_keyframe::Value;
use effectcraft_project::build::{self, Ids};
use effectcraft_project::{Comp, ItemId, ItemKind, Layer, LayerSource, Project, Solid};
use effectcraft_raster::channels3d::{BACKGROUND_DEPTH, crypto_hash};
use effectcraft_time::{FrameRate, Tick};
use crate::{NoFootage, RenderOpts, Renderer, render_frame};
const W: u32 = 200;
const H: u32 = 100;
fn zoom() -> f64 {
W as f64 * 50.0 / 36.0
}
fn solid3(p: &mut Project, comp: &Comp, name: &str, color: [f32; 3], pos: [f64; 3]) -> Layer {
let sid = p.add_item(name, Label::Red, None, ItemKind::Solid(Solid { color, width: 60, height: 60, pixel_aspect: 1.0 }));
let mut l = build::layer(p, comp, name, LayerSource::Solid { item: sid }, (60, 60), None);
l.name = name.to_string();
l.switches.three_d = true;
l.props.prop_mut("transform/position").unwrap().value = Value::Vec3(pos);
l
}
/// Inner 3D comp: "Near" (red, z 0, left) above "Far" (green, z 500, right). Outer comp: the
/// precomp layer, optionally with an effect.
fn scene(effect: Option<(&str, Vec<(&str, Value)>)>) -> (Project, ItemId, ItemId) {
let mut p = Project::default();
p.settings.bit_depth = effectcraft_project::BitDepth::Bpc32;
let inner = Comp::new(W, H, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
let iid = p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.clone().into()));
let near = solid3(&mut p, &inner, "Near", [1.0, 0.0, 0.0], [60.0, 50.0, 0.0]);
let far = solid3(&mut p, &inner, "Far", [0.0, 1.0, 0.0], [140.0, 50.0, 500.0]);
p.comp_mut(iid).unwrap().layers.push(near);
p.comp_mut(iid).unwrap().layers.push(far);
let outer = Comp::new(W, H, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
let oid = p.add_item("Outer", Label::Sandstone, None, ItemKind::Comp(outer.clone().into()));
let mut pre = build::layer(&mut p, &outer, "Inner", LayerSource::Comp { item: iid }, (W, H), None);
if let Some((id, vals)) = effect {
let spec = effectcraft_effects::find(id).unwrap();
let mut next = p.next_id;
let mut g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), spec.name, [W as f64, H as f64]);
p.next_id = next;
for (k, v) in vals {
g.prop_mut(k).unwrap().value = v.clone();
}
pre.props.sub_mut("effects").unwrap().children.push(g.into());
}
p.comp_mut(oid).unwrap().layers.push(pre);
(p, iid, oid)
}
#[test]
fn comp_aux_pass_has_depth_ids_and_cryptomatte() {
let (p, iid, _) = scene(None);
let r = Renderer::new(&p, &NoFootage, RenderOpts::default());
let a = r.comp_aux(iid, Tick::ZERO).unwrap();
assert_eq!((a.width, a.height), (W, H));
let at = |name: &str, x: usize, y: usize| a.get(name).unwrap()[y * W as usize + x];
// Near plane at the camera's zoom distance, the far one 500 px behind it.
assert!((at("Z", 60, 50) as f64 - zoom()).abs() < 1e-2, "{}", at("Z", 60, 50));
assert!((at("Z", 114, 50) as f64 - (zoom() + 500.0)).abs() < 1e-2, "{}", at("Z", 114, 50));
assert_eq!(at("Z", 5, 5), BACKGROUND_DEPTH);
assert_eq!((at("ObjectID", 60, 50), at("ObjectID", 114, 50), at("ObjectID", 5, 5)), (1.0, 2.0, 0.0));
assert_eq!(at("Coverage", 60, 50), 1.0);
// Planes face the camera.
assert!((at("N.Z", 60, 50) + 1.0).abs() < 1e-5);
// Cryptomatte rank 0 = the layer under the pixel, full coverage; manifest lists names.
assert_eq!(at("CryptoObject00.R", 60, 50).to_bits(), crypto_hash("Near"));
assert_eq!(at("CryptoObject00.G", 60, 50), 1.0);
assert_eq!(a.manifest_name("CryptoObject", crypto_hash("Far")), Some("Far"));
}
#[test]
fn three_d_channel_effects_work_on_a_precomp_of_a_3d_comp() {
let near = (60, 50);
let far = (114, 50);
let alpha = |p: &Project, oid: ItemId, (x, y): (i64, i64)| render_frame(p, oid, Tick::ZERO, 1.0).get(x, y)[3];
let (p, _, oid) = scene(Some(("ec.3d.idmatte", vec![("idSelection", Value::Scalar(2.0))])));
assert_eq!(alpha(&p, oid, near), 0.0);
assert!(alpha(&p, oid, far) > 0.99);
let (p, _, oid) = scene(Some(("ec.3d.depthmatte", vec![("depth", Value::Scalar(zoom() + 250.0))])));
assert_eq!(alpha(&p, oid, near), 0.0);
assert!(alpha(&p, oid, far) > 0.99);
let (p, _, oid) = scene(Some(("ec.3d.cryptomatte", vec![("selection", Value::Str("Near".into())), ("display", Value::Enum(1))])));
assert!(alpha(&p, oid, near) > 0.99);
assert_eq!(alpha(&p, oid, far), 0.0);
// Half resolution: the pass follows the render scale.
let (p, iid, _) = scene(None);
let r = Renderer::new(&p, &NoFootage, RenderOpts { scale: 0.5, ..Default::default() });
let a = r.comp_aux(iid, Tick::ZERO).unwrap();
assert_eq!((a.width, a.height, a.scale), (W / 2, H / 2, 0.5));
assert_eq!(a.get("ObjectID").unwrap()[25 * 100 + 30], 1.0);
}