//! GPU 3D Channel family vs the CPU effects (the oracle): direct on a buffer (full and half //! resolution, padded offsets, aux channels at 1× and 2× the layer resolution) and composited //! at 8 and 32 bpc from footage that carries auxiliary channels. use std::sync::Arc; use effectcraft_effects::{EffectEnv, EffectHost, LayerPixels}; use effectcraft_keyframe::Value; use effectcraft_project::{BitDepth, Footage, ItemId}; use effectcraft_raster::channels3d::{BACKGROUND_DEPTH, crypto_float, crypto_hash}; use effectcraft_raster::{AuxChannels, Image}; use effectcraft_render::{Backend, FootageSource, RenderOpts, Renderer}; use effectcraft_time::Tick; use crate::tests::{Pattern, Scene, c, check, diff, gpu, n, opts, pattern, set, tolerance}; pub(crate) struct AuxHost(pub(crate) Arc); impl EffectHost for AuxHost { fn layer(&self, _: u64, _: bool) -> Option { None } fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option> { None } fn aux(&self) -> Option> { Some(self.0.clone()) } } const NAMES: [&str; 5] = ["alpha", "beta", "gamma", "delta", "epsilon"]; /// Aux channels for a `w`×`h` layer at `scale` aux pixels per layer pixel: a depth ramp with a /// background corner, object / material ID blocks, coverage, UVs, normals, background RGB, raw /// RGB and a two-rank Cryptomatte layer with a manifest. pub(crate) fn aux_scene(w: u32, h: u32, scale: f64) -> AuxChannels { let (aw, ah) = ((w as f64 * scale) as u32, (h as f64 * scale) as u32); let mut a = AuxChannels::new(aw, ah, scale); let n = (aw * ah) as usize; let mut ch: Vec<(String, Vec)> = [ "Z", "ObjectID", "MaterialID", "Coverage", "UV.U", "UV.V", "N.X", "N.Y", "N.Z", "BG.R", "BG.G", "BG.B", "R", "G", "B", "CryptoObject00.R", "CryptoObject00.G", "CryptoObject00.B", "CryptoObject00.A", ] .iter() .map(|s| (s.to_string(), vec![0.0; n])) .collect(); let hashes: Vec = NAMES.iter().map(|s| crypto_hash(s)).collect(); for y in 0..ah { for x in 0..aw { let i = (y * aw + x) as usize; let (u, v) = (x as f32 / aw as f32, y as f32 / ah as f32); let bg = u > 0.8 && v < 0.25; let obj = ((x as f64 / scale) as u32 / 11 + (y as f64 / scale) as u32 / 9 * 3) % 5; let vals = [ if bg { BACKGROUND_DEPTH } else { 50.0 + 900.0 * u + 300.0 * (v * 7.0).sin() }, if bg { 0.0 } else { obj as f32 + 1.0 }, ((obj * 7) % 3) as f32, 0.5 + 0.5 * (u * 9.0).sin(), u, v, (u - 0.5) * 1.6, (0.5 - v) * 1.4, 0.7, 0.2 + 0.6 * v, 0.4, 0.9 - 0.5 * u, u * 1.8, v * 0.7, 0.3 + u * v * 2.5, crypto_float(hashes[obj as usize]), 0.7, crypto_float(hashes[(obj as usize + 1) % 5]), 0.3, ]; for (k, val) in vals.iter().enumerate() { ch[k].1[i] = *val; } } } a.channels = ch; a.manifests = vec![("CryptoObject".into(), NAMES.iter().zip(&hashes).map(|(n, h)| (n.to_string(), *h)).collect())]; a } /// The effect alone on a buffer with aux channels (`allow` = fraction of pixels that may /// differ by more than 1e-3). pub(crate) fn direct(id: &str, vals: &[(&str, Value)], allow: f64) { let Some(g) = gpu() else { return }; let spec = effectcraft_effects::find(id).unwrap(); let size = [70.0, 44.0]; let mut params = effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() }; for (k, v) in vals { params.values.insert(k.to_string(), v.clone()); } for aux_scale in [1.0, 2.0] { let host = AuxHost(Arc::new(aux_scene(70, 44, aux_scale))); for (scale, offset) in [(1.0, [0.0, 0.0]), (0.5, [0.0, 0.0]), (1.0, [3.0, 2.0])] { let (w, h) = ((size[0] * scale) as u32, (size[1] * scale) as u32); let mut img = pattern(7, w + 2 * offset[0] as u32, h + 2 * offset[1] as u32); if offset[0] > 0.0 { // A padded buffer: transparent margins. for (i, px) in img.data.iter_mut().enumerate() { let (x, y) = (i as u32 % img.width, i as u32 / img.width); if x < offset[0] as u32 || y < offset[1] as u32 || x >= w + offset[0] as u32 || y >= h + offset[1] as u32 { *px = [0.0; 4]; } } } let buf = effectcraft_effects::Buf { img, offset, scale }; let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: 0.25, layer_size: size, seed: 11, adjustment: false, env: EffectEnv { host: Some(&host), ..Default::default() }, }; let cpu = (spec.render)(&ctx(), buf.clone()); let out = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect"); assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry"); let d = diff(&cpu.img, &out.img, 1e-3); assert!( d.over as f64 <= allow * d.total as f64, "{id} {vals:?} aux {aux_scale} scale {scale} offset {offset:?}: {} of {} pixels over 1e-3 (max {}); worst {:?}", d.over, d.total, d.max, d.worst ); } } } /// Footage frames from [`Pattern`] with [`aux_scene`] channels. struct AuxSource; impl FootageSource for AuxSource { fn frame(&self, item: ItemId, f: &Footage, t: Tick) -> Option> { Pattern.frame(item, f, t) } fn aux(&self, _item: ItemId, f: &Footage, _t: Tick) -> Option> { Some(Arc::new(aux_scene(f.width, f.height, 1.0))) } } fn compare(s: &Scene, o: RenderOpts) -> Option { let g = gpu()?; let cpu = Renderer::new(&s.p, &AuxSource, RenderOpts { backend: Backend::Cpu, ..o }).comp_frame_cpu(s.cid, Tick::ZERO); let mut r = Renderer::new(&s.p, &AuxSource, RenderOpts { backend: Backend::Gpu, ..o }); r.accel = Some(g); let img = g.render(&r, s.cid, Tick::ZERO).expect("the GPU renders the frame"); let mut d = diff(&cpu, &img, tolerance(s.p.settings.bit_depth)); d.quantized = s.p.settings.bit_depth != BitDepth::Bpc32; Some(d) } /// Direct, then on a rotated footage layer over a background at 8 and 32 bpc (full and half /// resolution). fn case(id: &str, vals: &[(&str, Value)]) { direct(id, vals, 0.0); composited(id, vals, 0.0); } fn composited(id: &str, vals: &[(&str, Value)], allow: f64) { for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { let mut s = Scene::new(depth); let bg = s.solid([0.15, 0.1, 0.2], 97, 61); let bg_id = s.push(bg); let mut l = s.footage(70, 44); let mut vals = vals.to_vec(); for v in vals.iter_mut() { if v.1 == Value::Layer(Some(0)) { v.1 = Value::Layer(Some(bg_id.0)); } } s.effect(&mut l, id, &vals); set(&mut l, "transform/rotation", Value::Scalar(8.0)); s.push(l); check(&format!("{id} {vals:?} {depth:?}"), compare(&s, opts()), allow); check(&format!("{id} {vals:?} {depth:?} half"), compare(&s, RenderOpts { scale: 0.5, ..opts() }), allow); } } fn e(v: u32) -> Value { Value::Enum(v) } fn on() -> Value { Value::Bool(true) } #[test] fn channel_extract() { case("ec.3d.channelextract", &[("blackPoint", n(0.0)), ("whitePoint", n(1200.0))]); case("ec.3d.channelextract", &[("whitePoint", n(400.0)), ("clampOutput", Value::Bool(false)), ("invertDepthMap", on())]); case("ec.3d.channelextract", &[("blackPoint", n(900.0)), ("whitePoint", n(100.0))]); case("ec.3d.channelextract", &[("channel", e(1)), ("whitePoint", n(5.0))]); case("ec.3d.channelextract", &[("channel", e(2)), ("whitePoint", n(1.0))]); case("ec.3d.channelextract", &[("channel", e(3)), ("whitePoint", n(1.0))]); case("ec.3d.channelextract", &[("channel", e(4)), ("whitePoint", n(1.0)), ("antialias", on())]); case("ec.3d.channelextract", &[("channel", e(5)), ("whitePoint", n(1.0))]); case("ec.3d.channelextract", &[("channel", e(6))]); case("ec.3d.channelextract", &[("channel", e(7)), ("whitePoint", n(2.0))]); case("ec.3d.channelextract", &[("blackPoint", n(0.0)), ("whitePoint", n(1000.0)), ("antialias", on())]); } #[test] fn depth_matte_fog_and_dof() { case("ec.3d.depthmatte", &[("depth", n(500.0))]); case("ec.3d.depthmatte", &[("depth", n(400.0)), ("feather", n(300.0)), ("invert", on())]); case("ec.3d.fog3d", &[]); case("ec.3d.fog3d", &[("fogColor", c(0.3, 0.5, 0.9)), ("fogStartDepth", n(200.0)), ("fogEndDepth", n(900.0)), ("scatteringDensity", n(10.0))]); case("ec.3d.fog3d", &[("foggyBackground", Value::Bool(false)), ("fogOpacity", n(70.0))]); composited("ec.3d.fog3d", &[("gradientLayer", Value::Layer(Some(0))), ("layerContribution", n(60.0))], 0.0); case("ec.3d.depthoffield", &[("focalPlane", n(400.0)), ("maximumRadius", n(8.0))]); case("ec.3d.depthoffield", &[("focalPlane", n(200.0)), ("maximumRadius", n(5.0)), ("focalPlaneThickness", n(150.0)), ("focalBias", n(80.0))]); } #[test] fn ids_and_cryptomatte() { case("ec.3d.idmatte", &[("idSelection", n(2.0))]); case("ec.3d.idmatte", &[("idSelection", n(3.0)), ("feather", n(3.0)), ("useCoverage", on()), ("invert", on())]); case("ec.3d.idmatte", &[("auxChannel", e(1)), ("idSelection", n(1.0))]); case("ec.3d.identifier", &[]); case("ec.3d.identifier", &[("id", n(2.0))]); case("ec.3d.identifier", &[("display", e(1)), ("id", n(4.0))]); case("ec.3d.identifier", &[("display", e(2)), ("id", n(1.0))]); case("ec.3d.identifier", &[("display", e(3)), ("channelType", e(1))]); case("ec.3d.cryptomatte", &[("selection", Value::Str("beta, delta".into()))]); case("ec.3d.cryptomatte", &[("selection", Value::Str("*a".into())), ("display", e(0))]); case("ec.3d.cryptomatte", &[("selection", Value::Str("gamma".into())), ("display", e(1))]); case("ec.3d.cryptomatte", &[("selection", Value::Str("alpha".into())), ("matteOnly", on())]); } #[test] fn extractor() { case("ec.3d.extractor", &[("red", Value::Str("Z".into())), ("green", Value::Str("UV.U".into())), ("whitePoint", n(1000.0))]); case( "ec.3d.extractor", &[("red", Value::Str("G".into())), ("blue", Value::Str("Coverage".into())), ("alpha", Value::Str("UV.V".into())), ("unMult", on())], ); case("ec.3d.extractor", &[("red", Value::Str("A".into())), ("blackPoint", n(0.2)), ("whitePoint", n(0.8)), ("clip", on())]); case("ec.3d.extractor", &[("green", Value::Str("".into())), ("alpha", Value::Str("nothing".into()))]); }