mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
M13.23: GPU 3D Channel effects (aux channels as textures)
3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR, Fog 3D, ID Matte and IDentifier run on the GPU (fx_depth.rs / fx_depth.wgsl): aux planes upload as a texture and are resampled onto the buffer grid with the CPU's index arithmetic; tests compare against the CPU at 1x / 2x aux resolution, padded buffers, half resolution and 8 / 32 bpc composites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
081ce79de3
commit
6b91afc087
@@ -53,7 +53,7 @@ fn plane_of(b: &Buf, idx: &[Option<usize>], ch: &[f32], fill: f32) -> Plane {
|
||||
}
|
||||
|
||||
/// Distinct colour for an ID (hash → hue).
|
||||
fn id_color(id: f32) -> [f32; 3] {
|
||||
pub fn id_color(id: f32) -> [f32; 3] {
|
||||
let h = hash1(id.to_bits(), 0x1d, 0x5eed);
|
||||
let (r, g, b) = effectcraft_color::hsl_to_rgb(h, 0.75, 0.5);
|
||||
if id == 0.0 { [0.0; 3] } else { [r, g, b] }
|
||||
|
||||
@@ -97,6 +97,8 @@ pub use noise3::{FractalGpu, fractal_gpu};
|
||||
pub use time_fx::{posterized_time, time_frames};
|
||||
// effectcraft-gpu fx_tone.
|
||||
pub use ocio::color_stabilizer_maps;
|
||||
// GPU effects, part B (3D Channel, Immersive Video, colour management, simulation render passes).
|
||||
pub use channel3d::{CRYPTO_LAYERS, id_color, selection_hashes};
|
||||
|
||||
/// Effect categories in Effects & Presets order.
|
||||
pub const CATEGORIES: &[&str] = &[
|
||||
@@ -931,6 +933,15 @@ pub const GPU_EFFECTS: &[&str] = &[
|
||||
"ec.perspective.bevelalpha",
|
||||
"ec.perspective.beveledges",
|
||||
"ec.obsolete.gaussianlegacy",
|
||||
// GPU effects, part B: 3D Channel.
|
||||
"ec.3d.channelextract",
|
||||
"ec.3d.cryptomatte",
|
||||
"ec.3d.depthmatte",
|
||||
"ec.3d.depthoffield",
|
||||
"ec.3d.extractor",
|
||||
"ec.3d.fog3d",
|
||||
"ec.3d.idmatte",
|
||||
"ec.3d.identifier",
|
||||
];
|
||||
|
||||
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
||||
|
||||
@@ -314,6 +314,7 @@ impl GpuContext {
|
||||
include_str!("shaders/fx_tone.wgsl"),
|
||||
include_str!("shaders/fx_warp.wgsl"),
|
||||
include_str!("shaders/fx_extra.wgsl"),
|
||||
include_str!("shaders/fx_depth.wgsl"),
|
||||
]
|
||||
.concat();
|
||||
let module =
|
||||
@@ -373,6 +374,7 @@ impl GpuContext {
|
||||
.chain(crate::fx_tone::KERNELS)
|
||||
.chain(crate::fx_warp::KERNELS)
|
||||
.chain(crate::fx_extra::KERNELS)
|
||||
.chain(crate::fx_depth::KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| {
|
||||
|
||||
@@ -133,6 +133,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
_ if crate::fx_tone::IDS.contains(&id) => crate::fx_tone::apply(e, id, ctx, b),
|
||||
_ if crate::fx_warp::IDS.contains(&id) => crate::fx_warp::apply(e, id, ctx, b),
|
||||
_ if crate::fx_extra::IDS.contains(&id) => crate::fx_extra::apply(e, id, ctx, b),
|
||||
_ if crate::fx_depth::IDS.contains(&id) => crate::fx_depth::apply(e, id, ctx, b),
|
||||
_ => pointwise(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
//! GPU effects, 3D Channel family (kernels in `shaders/fx_depth.wgsl`, every entry point
|
||||
//! prefixed `fxd_`): 3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR,
|
||||
//! Fog 3D, ID Matte and IDentifier.
|
||||
//!
|
||||
//! The layer's auxiliary channels ([`effectcraft_raster::AuxChannels`]) go to the GPU as an
|
||||
//! extra texture at the aux resolution: the planes an effect reads are packed into its four
|
||||
//! channels (alpha = 1 marks "inside the aux image"). `fxd_lookup` resamples it nearest-neighbour
|
||||
//! onto the buffer grid with the CPU's exact index arithmetic (layer position × aux scale,
|
||||
//! floored), and the effect kernels work on that buffer-sized plane. Cryptomatte packs a
|
||||
//! per-aux-pixel reduction of its rank layers (selection coverage and the coverage-weighted ID
|
||||
//! colours, measured on the CPU) instead of the raw ranks. Fog 3D's Gradient Layer is rendered
|
||||
//! by the host on the CPU (it is another layer) and uploaded.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectCtx};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_raster::channels3d::BACKGROUND_DEPTH;
|
||||
use effectcraft_raster::{AuxChannels, Image};
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_depth.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["fxd_lookup", "fxd_point", "fxd_dof_f", "fxd_dof_level", "fxd_avg"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.3d.channelextract",
|
||||
"ec.3d.cryptomatte",
|
||||
"ec.3d.depthmatte",
|
||||
"ec.3d.depthoffield",
|
||||
"ec.3d.extractor",
|
||||
"ec.3d.fog3d",
|
||||
"ec.3d.idmatte",
|
||||
"ec.3d.identifier",
|
||||
];
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let aux = ctx.env.host.and_then(|h| h.aux());
|
||||
if id == "ec.3d.extractor" {
|
||||
return extractor(e, ctx, b, aux.as_deref());
|
||||
}
|
||||
let Some(a) = aux else { return Some(b) };
|
||||
match id {
|
||||
"ec.3d.channelextract" => channel_extract(e, ctx, b, &a),
|
||||
"ec.3d.cryptomatte" => cryptomatte(e, ctx, b, &a),
|
||||
"ec.3d.depthmatte" => depth_matte(e, ctx, b, &a),
|
||||
"ec.3d.depthoffield" => depth_of_field(e, ctx, b, &a),
|
||||
"ec.3d.fog3d" => fog(e, ctx, b, &a),
|
||||
"ec.3d.idmatte" => id_matte(e, ctx, b, &a),
|
||||
"ec.3d.identifier" => identifier(e, ctx, b, &a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn op(code: u32) -> Params {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = code;
|
||||
p
|
||||
}
|
||||
|
||||
/// Pack up to four aux planes (aux resolution; `None` planes are 0, alpha defaults to 1 = inside)
|
||||
/// and upload them.
|
||||
fn aux_texture(e: &mut Enc, a: &AuxChannels, planes: [Option<&[f32]>; 4]) -> Option<GpuImage> {
|
||||
let n = (a.width * a.height) as usize;
|
||||
if planes.iter().flatten().any(|p| p.len() < n) {
|
||||
return None;
|
||||
}
|
||||
let mut img = Image::new(a.width, a.height);
|
||||
for (i, px) in img.data.iter_mut().enumerate() {
|
||||
*px = [0, 1, 2, 3].map(|k| planes[k].map_or(if k == 3 { 1.0 } else { 0.0 }, |p| p[i]));
|
||||
}
|
||||
e.g.upload_image(&img)
|
||||
}
|
||||
|
||||
/// Resample an aux texture onto the buffer grid (nearest; `fill` outside the aux image),
|
||||
/// sampling each buffer pixel at sub-pixel position `at`.
|
||||
fn lookup(e: &mut Enc, b: &GBuf, a: &AuxChannels, tex: &GpuImage, at: [f64; 2], fill: [f32; 4]) -> GpuImage {
|
||||
let inv = 1.0 / b.scale.max(1e-9);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [b.offset[0] as f32, b.offset[1] as f32, inv as f32, a.scale as f32];
|
||||
p.f[1] = [at[0] as f32, at[1] as f32, 0.0, 0.0];
|
||||
p.f[2] = fill;
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxd_lookup", &p, tex, None, &out, None);
|
||||
out
|
||||
}
|
||||
|
||||
fn point(e: &mut Enc, p: &Params, b: GBuf, plane: &GpuImage) -> Option<GBuf> {
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxd_point", p, &b.img, Some(plane), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 3D Channel Extract
|
||||
|
||||
fn channel_extract(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let get3 = |n: [&[&str]; 3]| -> Option<[&[f32]; 3]> { Some([a.find(n[0])?, a.find(n[1])?, a.find(n[2])?]) };
|
||||
let u = ["UV.U", "U", "uv.x"];
|
||||
let v = ["UV.V", "V", "uv.y"];
|
||||
// (planes, mode): mode 0 grey (lv), 1 UV (lv on R, G; B = 0), 2 normals (lv of ×0.5 + 0.5),
|
||||
// 3 RGB (lv), 4 unclamped RGB (no mapping).
|
||||
let (planes, mode): ([&[f32]; 3], u32) = match pr.e("channel") {
|
||||
0 => (a.depth().map(|z| [z, z, z])?, 0),
|
||||
1 => (a.object_id().map(|c| [c, c, c])?, 0),
|
||||
7 => (a.material_id().map(|c| [c, c, c])?, 0),
|
||||
4 => (a.coverage().map(|c| [c, c, c])?, 0),
|
||||
2 => (get3([&u, &v, &["UV.W", "W", "uv.z"]]).or_else(|| Some([a.find(&u)?, a.find(&v)?, a.find(&v)?]))?, 1),
|
||||
3 => (get3([&["N.X", "Normal.X", "normals.x"], &["N.Y", "Normal.Y", "normals.y"], &["N.Z", "Normal.Z", "normals.z"]])?, 2),
|
||||
5 => (get3([&["BG.R", "Background.R"], &["BG.G", "Background.G"], &["BG.B", "Background.B"]])?, 3),
|
||||
_ => (get3([&["R"], &["G"], &["B"]])?, 4),
|
||||
};
|
||||
let tex = aux_texture(e, a, [Some(planes[0]), Some(planes[1]), Some(planes[2]), None])?;
|
||||
let mut p = op(0);
|
||||
p.u[0] = [0, mode, pr.b("clampOutput") as u32, pr.b("invertDepthMap") as u32];
|
||||
p.f[0] = [pr.f("blackPoint") as f32, pr.f("whitePoint") as f32, 0.0, 0.0];
|
||||
if !pr.b("antialias") {
|
||||
let plane = lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]);
|
||||
return point(e, &p, b, &plane);
|
||||
}
|
||||
// Anti-alias: four sub-pixel samples, summed in order and scaled once (as the CPU does).
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut acc: Option<GpuImage> = None;
|
||||
for s in [[0.25, 0.25], [0.75, 0.25], [0.25, 0.75], [0.75, 0.75]] {
|
||||
let plane = lookup(e, &b, a, &tex, s, [0.0; 4]);
|
||||
let one = e.scratch(w, h);
|
||||
e.pixels("fxd_point", &p, &b.img, Some(&plane), &one, None);
|
||||
acc = Some(match acc {
|
||||
None => one,
|
||||
Some(prev) => {
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxd_avg", &op(0), &prev, Some(&one), &out, None);
|
||||
out
|
||||
}
|
||||
});
|
||||
}
|
||||
let acc = acc?;
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxd_avg", &op(1), &acc, None, &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Depth Matte / Fog 3D
|
||||
|
||||
fn depth_plane(e: &mut Enc, b: &GBuf, a: &AuxChannels) -> Option<GpuImage> {
|
||||
let z = a.depth()?;
|
||||
let tex = aux_texture(e, a, [Some(z), None, None, None])?;
|
||||
Some(lookup(e, b, a, &tex, [0.5, 0.5], [BACKGROUND_DEPTH, 0.0, 0.0, 0.0]))
|
||||
}
|
||||
|
||||
fn depth_matte(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let Some(plane) = depth_plane(e, &b, a) else { return if a.depth().is_none() { Some(b) } else { None } };
|
||||
let mut p = op(1);
|
||||
p.u[0][1] = ctx.params.b("invert") as u32;
|
||||
p.f[0] = [ctx.params.f("depth") as f32, ctx.params.f("feather").max(0.0) as f32, 0.0, 0.0];
|
||||
point(e, &p, b, &plane)
|
||||
}
|
||||
|
||||
fn fog(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let Some(plane) = depth_plane(e, &b, a) else { return if a.depth().is_none() { Some(b) } else { None } };
|
||||
let contrib = (pr.f("layerContribution") / 100.0) as f32;
|
||||
let grad = if contrib > 0.0 && matches!(pr.get("gradientLayer"), Some(Value::Layer(Some(_)))) {
|
||||
ctx.layer_param("gradientLayer", true).map(|o| {
|
||||
let cpu = Buf { img: Image::new(b.img.width, b.img.height), offset: b.offset, scale: b.scale };
|
||||
effectcraft_effects::util::fit_layer(ctx, &cpu, &o, true)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let grad = match grad {
|
||||
Some(g) => Some(e.g.upload_image(&g)?),
|
||||
None => None,
|
||||
};
|
||||
let fc = pr.color("fogColor");
|
||||
let mut p = op(2);
|
||||
p.u[0] = [2, pr.b("foggyBackground") as u32, grad.is_some() as u32, 0];
|
||||
p.f[0] = [pr.f("fogStartDepth") as f32, pr.f("fogEndDepth") as f32, (pr.f("fogOpacity") / 100.0) as f32, (pr.f("scatteringDensity") / 100.0) as f32];
|
||||
p.f[1] = [fc[0], fc[1], fc[2], contrib];
|
||||
p.f[2][0] = BACKGROUND_DEPTH;
|
||||
let Some(g) = grad else { return point(e, &p, b, &plane) };
|
||||
// The gradient's factor joins the depth plane (G) first: kernels read two images.
|
||||
let mut q = op(3);
|
||||
q.f[0][0] = contrib;
|
||||
let merged = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxd_point", &q, &g, Some(&plane), &merged, None);
|
||||
point(e, &p, b, &merged)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Depth of Field
|
||||
|
||||
fn depth_of_field(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
if a.depth().is_none() {
|
||||
return Some(b);
|
||||
}
|
||||
let max_r = (pr.f("maximumRadius").max(0.0) * b.scale) as f32;
|
||||
if max_r <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let plane = depth_plane(e, &b, a)?;
|
||||
const LEVELS: usize = 5;
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let stack: Vec<GpuImage> = (0..LEVELS)
|
||||
.map(|k| {
|
||||
let r = max_r * k as f32 / (LEVELS - 1) as f32;
|
||||
if r < 0.3 { b.img.clone() } else { gaussian_blur(e, &b.img, (r / 2.0) as f64, (r / 2.0) as f64, true) }
|
||||
})
|
||||
.collect();
|
||||
// Per-pixel stack position, then each level pair writes its own pixels.
|
||||
let mut p = Params::default();
|
||||
let gamma = 2f32.powf((50.0 - pr.f("focalBias") as f32) / 25.0);
|
||||
p.f[0] = [pr.f("focalPlane") as f32, pr.f("focalPlaneThickness").max(0.0) as f32, max_r, gamma];
|
||||
p.u[0][0] = LEVELS as u32;
|
||||
let fpl = e.scratch(w, h);
|
||||
e.pixels("fxd_dof_f", &p, &plane, None, &fpl, None);
|
||||
let (rows, row_len) = e.image_rows(&fpl);
|
||||
let out = e.scratch(w, h);
|
||||
for k in 0..LEVELS - 1 {
|
||||
let mut q = Params::default();
|
||||
q.u[0] = [k as u32, row_len, LEVELS as u32, 0];
|
||||
e.pixels("fxd_dof_level", &q, &stack[k], Some(&stack[k + 1]), &out, Some(&rows));
|
||||
}
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- ID Matte / IDentifier
|
||||
|
||||
fn id_plane(a: &AuxChannels, which: u32) -> Option<&[f32]> {
|
||||
if which == 1 { a.material_id() } else { a.object_id() }
|
||||
}
|
||||
|
||||
fn id_matte(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let Some(ids) = id_plane(a, pr.e("auxChannel")) else { return Some(b) };
|
||||
let cov = if pr.b("useCoverage") { a.coverage() } else { None };
|
||||
let tex = aux_texture(e, a, [Some(ids), cov, None, None])?;
|
||||
let plane = lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut p = op(4);
|
||||
p.u[0][1] = cov.is_some() as u32;
|
||||
p.f[0][0] = pr.f("idSelection").round() as f32;
|
||||
let mut m = e.scratch(w, h);
|
||||
e.pixels("fxd_point", &p, &plane, None, &m, None);
|
||||
let feather = pr.f("feather").max(0.0) * b.scale;
|
||||
if feather > 0.0 {
|
||||
m = gaussian_blur(e, &m, feather * 0.5, feather * 0.5, true);
|
||||
}
|
||||
let mut p = op(5);
|
||||
p.u[0][1] = pr.b("invert") as u32;
|
||||
point(e, &p, b, &m)
|
||||
}
|
||||
|
||||
fn identifier(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: &AuxChannels) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let Some(ids) = id_plane(a, pr.e("channelType")) else { return Some(b) };
|
||||
let max_id = ids.iter().cloned().fold(1.0f32, f32::max);
|
||||
let tex = aux_texture(e, a, [Some(ids), None, None, None])?;
|
||||
let plane = lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]);
|
||||
let mut p = op(6);
|
||||
p.u[0][1] = pr.e("display");
|
||||
p.f[0] = [pr.f("id").round() as f32, max_id, 0.0, 0.0];
|
||||
point(e, &p, b, &plane)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Cryptomatte
|
||||
|
||||
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 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).
|
||||
let n = (a.width * a.height) as usize;
|
||||
let mut img = Image::new(a.width, a.height);
|
||||
for (i, px) in img.data.iter_mut().enumerate().take(n) {
|
||||
let ranks = a.crypto_ranks(&layer, i);
|
||||
let m: f32 = ranks.iter().filter(|(id, c)| *c > 0.0 && sel.contains(&id.to_bits())).map(|(_, c)| *c).sum::<f32>().clamp(0.0, 1.0);
|
||||
let mut c = [0.0f32; 3];
|
||||
for (id, cov) in &ranks {
|
||||
let k = effectcraft_effects::id_color(*id);
|
||||
for j in 0..3 {
|
||||
c[j] += k[j] * cov;
|
||||
}
|
||||
}
|
||||
*px = [c[0], c[1], c[2], m];
|
||||
}
|
||||
let tex = e.g.upload_image(&img)?;
|
||||
let plane = lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]);
|
||||
let mut p = op(7);
|
||||
p.u[0] = [7, pr.e("display"), pr.b("matteOnly") as u32, 0];
|
||||
point(e, &p, b, &plane)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- EXtractoR
|
||||
|
||||
fn extractor(e: &mut Enc, ctx: &EffectCtx, b: GBuf, a: Option<&AuxChannels>) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let names = [pr.s("red"), pr.s("green"), pr.s("blue"), pr.s("alpha")];
|
||||
let (bp, wp) = (pr.f("blackPoint") as f32, pr.f("whitePoint") as f32);
|
||||
let unmult = pr.b("unMult");
|
||||
let own = |n: &str| match n.trim().to_ascii_uppercase().as_str() {
|
||||
"R" | "RED" => Some(0u32),
|
||||
"G" | "GREEN" => Some(1),
|
||||
"B" | "BLUE" => Some(2),
|
||||
"A" | "ALPHA" => Some(3),
|
||||
_ => None,
|
||||
};
|
||||
let src_ch: Vec<Option<&[f32]>> = names.iter().map(|n| a.and_then(|a| if n.trim().is_empty() { None } else { a.get(n.trim()) })).collect();
|
||||
if src_ch.iter().all(Option::is_none) && bp == 0.0 && wp == 1.0 && !unmult && names.iter().enumerate().all(|(k, n)| own(n) == Some(k as u32)) {
|
||||
return Some(b);
|
||||
}
|
||||
// Per output channel: 0..3 = the layer's own channel, 4 = aux, 5 = default (0, alpha 1).
|
||||
let sources: [u32; 4] = std::array::from_fn(|k| if src_ch[k].is_some() { 4 } else { own(names[k]).unwrap_or(5) });
|
||||
let plane = match a {
|
||||
Some(a) if src_ch.iter().any(Option::is_some) => {
|
||||
let tex = aux_texture(e, a, [src_ch[0], src_ch[1], src_ch[2], src_ch[3]])?;
|
||||
Some(lookup(e, &b, a, &tex, [0.5, 0.5], [0.0; 4]))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let mut p = op(8);
|
||||
p.u[0] = [8, unmult as u32, pr.b("clip") as u32, 0];
|
||||
p.u[1] = sources;
|
||||
p.f[0] = [bp, wp, 0.0, 0.0];
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxd_point", &p, &b.img, plane.as_ref(), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
@@ -48,6 +48,7 @@ mod classic3d;
|
||||
mod context;
|
||||
mod effects;
|
||||
mod fx_color;
|
||||
mod fx_depth;
|
||||
mod fx_distort;
|
||||
mod fx_extra;
|
||||
mod fx_generate;
|
||||
@@ -201,6 +202,8 @@ mod tests_adv3d;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_color;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_depth;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_distort;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_extra;
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
// ---------------------------------------------------------------- 3D Channel family (fx_depth.rs)
|
||||
//
|
||||
// `aux` planes are buffer-sized resamplings of the layer's auxiliary channels (fxd_lookup); the
|
||||
// alpha of a plane is 1 inside the aux image and 0 outside (where the effect's fill applies).
|
||||
|
||||
fn fxd_unpremul(p: vec4<f32>) -> vec4<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return vec4<f32>(p.xyz / p.w, p.w);
|
||||
}
|
||||
return vec4<f32>(0.0, 0.0, 0.0, max(p.w, 0.0));
|
||||
}
|
||||
|
||||
// f32::signum (+0 → 1).
|
||||
fn fxd_signum(v: f32) -> f32 {
|
||||
return select(-1.0, 1.0, v >= 0.0);
|
||||
}
|
||||
|
||||
// Nearest aux texel under each buffer pixel (AuxChannels::index_at), `fill` outside.
|
||||
// f[0] = (offset x, offset y, 1 / buffer scale, aux scale); f[1].xy = sub-pixel position; f[2] = fill.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_lookup(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let lx = (f32(p.x) + P.f[1].x - P.f[0].x) * P.f[0].z;
|
||||
let ly = (f32(p.y) + P.f[1].y - P.f[0].y) * P.f[0].z;
|
||||
let ax = floor(lx * P.f[0].w);
|
||||
let ay = floor(ly * P.f[0].w);
|
||||
let ad = vec2<f32>(textureDimensions(src));
|
||||
var v = P.f[2];
|
||||
if (ax >= 0.0 && ay >= 0.0 && ax < ad.x && ay < ad.y) {
|
||||
v = textureLoad(src, vec2<i32>(i32(ax), i32(ay)), 0);
|
||||
}
|
||||
textureStore(out, p, v);
|
||||
}
|
||||
|
||||
// 3D Channel Extract's black / white point mapping.
|
||||
fn fxd_lv(v: f32) -> f32 {
|
||||
let bp = P.f[0].x;
|
||||
let wp = P.f[0].y;
|
||||
let clampo = P.u[0].z != 0u;
|
||||
var o: f32;
|
||||
if (wp >= bp) {
|
||||
o = (v - bp) / max(abs(wp - bp), 1e-9) * fxd_signum(wp - bp);
|
||||
if (clampo) {
|
||||
o = clamp(o, 0.0, 1.0);
|
||||
}
|
||||
} else {
|
||||
var q = (v - wp) / max(bp - wp, 1e-9);
|
||||
if (clampo) {
|
||||
q = clamp(q, 0.0, 1.0);
|
||||
}
|
||||
o = 1.0 - q;
|
||||
}
|
||||
if (P.u[0].w != 0u) {
|
||||
o = 1.0 - o;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// channel3d::id_color.
|
||||
fn fxd_id_color(id: f32) -> vec3<f32> {
|
||||
if (id == 0.0) {
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
let h = hash_noise(bitcast<u32>(id), 0x1du, 0x5eedu);
|
||||
return hsl_to_rgb(h, 0.75, 0.5);
|
||||
}
|
||||
|
||||
// u[0].x = op; the effects' per-pixel steps (src = layer, aux = plane unless noted).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let q = tex_get(aux, p.x, p.y);
|
||||
var o = px;
|
||||
switch (P.u[0].x) {
|
||||
// 3D Channel Extract. u[0] = (op, mode, clamp, invert); f[0] = (black, white).
|
||||
case 0u: {
|
||||
if (q.w == 0.0) {
|
||||
o = vec4<f32>(0.0);
|
||||
} else {
|
||||
var c = q.xyz;
|
||||
switch (P.u[0].y) {
|
||||
case 1u: {
|
||||
c = vec3<f32>(fxd_lv(c.x), fxd_lv(c.y), 0.0);
|
||||
}
|
||||
case 2u: {
|
||||
c = vec3<f32>(fxd_lv(c.x * 0.5 + 0.5), fxd_lv(c.y * 0.5 + 0.5), fxd_lv(c.z * 0.5 + 0.5));
|
||||
}
|
||||
case 4u: {}
|
||||
default: {
|
||||
c = vec3<f32>(fxd_lv(c.x), fxd_lv(c.y), fxd_lv(c.z));
|
||||
}
|
||||
}
|
||||
o = vec4<f32>(c, 1.0);
|
||||
}
|
||||
}
|
||||
// Depth Matte. u[0].y = invert; f[0] = (depth, feather).
|
||||
case 1u: {
|
||||
let zv = q.x;
|
||||
let depth = P.f[0].x;
|
||||
let feather = P.f[0].y;
|
||||
var k: f32;
|
||||
if (feather > 0.0) {
|
||||
k = clamp((zv - depth) / feather + 0.5, 0.0, 1.0);
|
||||
} else {
|
||||
k = select(0.0, 1.0, zv >= depth);
|
||||
}
|
||||
if (P.u[0].y != 0u) {
|
||||
k = 1.0 - k;
|
||||
}
|
||||
o = px * k;
|
||||
}
|
||||
// Fog 3D. u[0] = (op, foggy background, gradient in aux.y); f[0] = (start, end, opacity,
|
||||
// density); f[1] = (fog colour, contribution); f[2].x = background depth.
|
||||
case 2u: {
|
||||
let zv = q.x;
|
||||
let bg = zv >= P.f[2].x * 0.5;
|
||||
let foggy = P.u[0].y != 0u;
|
||||
var f: f32;
|
||||
if (bg) {
|
||||
f = select(0.0, 1.0, foggy);
|
||||
} else {
|
||||
let t = clamp((zv - P.f[0].x) / max(abs(P.f[0].y - P.f[0].x), 1e-6), 0.0, 1.0);
|
||||
f = 1.0 - powz(1.0 - t, 1.0 + P.f[0].w * 3.0);
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
f *= q.y;
|
||||
}
|
||||
f = clamp(f * P.f[0].z, 0.0, 1.0);
|
||||
let s = fxd_unpremul(px);
|
||||
var al = s.w;
|
||||
if (bg && foggy) {
|
||||
al = s.w + (1.0 - s.w) * f;
|
||||
}
|
||||
let c = s.xyz + (P.f[1].xyz - s.xyz) * f;
|
||||
o = vec4<f32>(c * al, al);
|
||||
}
|
||||
// Fog 3D gradient factor (src = fitted gradient layer, aux = depth plane). f[0].x = contribution.
|
||||
case 3u: {
|
||||
let g = fxd_unpremul(px);
|
||||
let l = luminance(g.xyz) * g.w;
|
||||
o = vec4<f32>(q.x, 1.0 - P.f[0].x + P.f[0].x * l, 0.0, 0.0);
|
||||
}
|
||||
// ID Matte's matte (src = plane). u[0].y = use coverage; f[0].x = ID.
|
||||
case 4u: {
|
||||
var m = 0.0;
|
||||
if (px.w != 0.0 && round_away(px.x) == P.f[0].x) {
|
||||
m = select(1.0, clamp(px.y, 0.0, 1.0), P.u[0].y != 0u);
|
||||
}
|
||||
o = vec4<f32>(m);
|
||||
}
|
||||
// ID Matte applied (aux = blurred matte). u[0].y = invert.
|
||||
case 5u: {
|
||||
var k = q.x;
|
||||
if (P.u[0].y != 0u) {
|
||||
k = 1.0 - k;
|
||||
}
|
||||
o = px * k;
|
||||
}
|
||||
// IDentifier. u[0].y = display; f[0] = (ID, largest ID).
|
||||
case 6u: {
|
||||
let display = P.u[0].y;
|
||||
if (q.w == 0.0) {
|
||||
if (display != 2u) {
|
||||
o = vec4<f32>(0.0);
|
||||
}
|
||||
} else {
|
||||
let id = round_away(q.x);
|
||||
let sel = P.f[0].x;
|
||||
let hit = id == sel;
|
||||
switch (display) {
|
||||
case 0u: {
|
||||
var c = fxd_id_color(id);
|
||||
if (hit && sel != 0.0) {
|
||||
c = vec3<f32>(1.0);
|
||||
}
|
||||
o = vec4<f32>(c, 1.0);
|
||||
}
|
||||
case 1u: {
|
||||
let v = select(0.0, 1.0, hit);
|
||||
o = vec4<f32>(v, v, v, 1.0);
|
||||
}
|
||||
case 2u: {
|
||||
o = select(vec4<f32>(0.0), px, hit);
|
||||
}
|
||||
default: {
|
||||
let v = id / P.f[0].y;
|
||||
o = vec4<f32>(v, v, v, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cryptomatte (aux = (ID colours, selection coverage)). u[0] = (op, display, matte only).
|
||||
case 7u: {
|
||||
let m = q.w;
|
||||
if (P.u[0].z != 0u) {
|
||||
o = vec4<f32>(m);
|
||||
} else if (P.u[0].y == 0u) {
|
||||
o = vec4<f32>(q.xyz * (1.0 - 0.5 * m) + 0.5 * m, 1.0);
|
||||
} else if (P.u[0].y == 1u) {
|
||||
o = px * m;
|
||||
} else {
|
||||
o = vec4<f32>(px.x * (1.0 - 0.5 * m) + 0.5 * m * px.w, px.y * (1.0 - 0.5 * m) + 0.5 * m * px.w, px.z * (1.0 - 0.5 * m), px.w);
|
||||
}
|
||||
}
|
||||
// EXtractoR. u[0] = (op, unmult, clip); u[1] = per-channel source (0..3 own, 4 aux, 5 default);
|
||||
// f[0] = (black, white).
|
||||
case 8u: {
|
||||
let s = fxd_unpremul(px);
|
||||
let straight = vec4<f32>(s.xyz, s.w);
|
||||
var v = vec4<f32>(0.0);
|
||||
for (var k = 0; k < 4; k++) {
|
||||
let kind = P.u[1][k];
|
||||
var x: f32;
|
||||
if (kind == 4u) {
|
||||
x = q[k];
|
||||
} else if (kind < 4u) {
|
||||
x = straight[kind];
|
||||
} else {
|
||||
x = select(0.0, 1.0, k == 3);
|
||||
}
|
||||
if (k < 3) {
|
||||
let bp = P.f[0].x;
|
||||
let wp = P.f[0].y;
|
||||
x = (x - bp) / max(abs(wp - bp), 1e-9) * fxd_signum(wp - bp);
|
||||
if (P.u[0].z != 0u) {
|
||||
x = clamp(x, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
v[k] = x;
|
||||
}
|
||||
let al = clamp(v.w, 0.0, 1.0);
|
||||
var c = v.xyz;
|
||||
if (P.u[0].y != 0u && al > 1e-6) {
|
||||
c = v.xyz / al;
|
||||
}
|
||||
o = vec4<f32>(c * al, al);
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// Depth of Field: each pixel's position in the blur stack (src = depth plane).
|
||||
// u[0].x = levels; f[0] = (focal plane, thickness, maximum radius, bias exponent).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_dof_f(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let z = textureLoad(src, p, 0).x;
|
||||
let focal = P.f[0].x;
|
||||
let max_r = P.f[0].z;
|
||||
let d = max(abs(z - focal) - P.f[0].y * 0.5, 0.0);
|
||||
let range = max(abs(focal), 1.0);
|
||||
let n = min(d / range, 1.0);
|
||||
let r = max_r * powz(n, P.f[0].w);
|
||||
let top = f32(P.u[0].x - 1u);
|
||||
let f = clamp(r / max_r * top, 0.0, top);
|
||||
textureStore(out, p, vec4<f32>(f, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// Depth of Field: pixels between stack levels k (src) and k + 1 (aux); the positions come
|
||||
// through `data` (rows of RGBA f32). u[0] = (k, row length, levels).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_dof_level(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let f = data[(u32(p.y) * P.u[0].y + u32(p.x)) * 4u];
|
||||
let k0 = min(u32(floor(f)), P.u[0].z - 2u);
|
||||
if (k0 != P.u[0].x) {
|
||||
return;
|
||||
}
|
||||
let t = f - f32(k0);
|
||||
let a0 = textureLoad(src, p, 0);
|
||||
let a1 = textureLoad(aux, p, 0);
|
||||
textureStore(out, p, a0 + (a1 - a0) * t);
|
||||
}
|
||||
|
||||
// u[0].x = 0: src + aux; 1: src × 0.25.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxd_avg(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0);
|
||||
if (P.u[0].x == 0u) {
|
||||
textureStore(out, p, a + textureLoad(aux, p, 0));
|
||||
} else {
|
||||
textureStore(out, p, a * 0.25);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//! 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<AuxChannels>);
|
||||
|
||||
impl EffectHost for AuxHost {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<LayerPixels> {
|
||||
None
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
fn aux(&self) -> Option<Arc<AuxChannels>> {
|
||||
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<f32>)> = [
|
||||
"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<u32> = 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<Arc<Image>> {
|
||||
Pattern.frame(item, f, t)
|
||||
}
|
||||
fn aux(&self, _item: ItemId, f: &Footage, _t: Tick) -> Option<Arc<AuxChannels>> {
|
||||
Some(Arc::new(aux_scene(f.width, f.height, 1.0)))
|
||||
}
|
||||
}
|
||||
|
||||
fn compare(s: &Scene, o: RenderOpts) -> Option<crate::tests::Diff> {
|
||||
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()))]);
|
||||
}
|
||||
+9
-9
@@ -5,7 +5,7 @@
|
||||
|
||||
EffectCraft ships 306 effects with 3009 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 166 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 174 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 306 implemented in full, 0 partial (what is missing is listed).
|
||||
|
||||
@@ -15,14 +15,14 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 3D Channel Extract | `ec.3d.channelextract` | 6 | | 32 | Implemented |
|
||||
| Cryptomatte | `ec.3d.cryptomatte` | 4 | | 32 | Implemented |
|
||||
| Depth Matte | `ec.3d.depthmatte` | 3 | | 32 | Implemented |
|
||||
| Depth of Field | `ec.3d.depthoffield` | 4 | | 32 | Implemented |
|
||||
| EXtractoR | `ec.3d.extractor` | 8 | | 32 | Implemented |
|
||||
| Fog 3D | `ec.3d.fog3d` | 8 | | 32 | Implemented |
|
||||
| ID Matte | `ec.3d.idmatte` | 5 | | 32 | Implemented |
|
||||
| IDentifier | `ec.3d.identifier` | 3 | | 32 | Implemented |
|
||||
| 3D Channel Extract | `ec.3d.channelextract` | 6 | GPU | 32 | Implemented |
|
||||
| Cryptomatte | `ec.3d.cryptomatte` | 4 | GPU | 32 | Implemented |
|
||||
| Depth Matte | `ec.3d.depthmatte` | 3 | GPU | 32 | Implemented |
|
||||
| Depth of Field | `ec.3d.depthoffield` | 4 | GPU | 32 | Implemented |
|
||||
| EXtractoR | `ec.3d.extractor` | 8 | GPU | 32 | Implemented |
|
||||
| Fog 3D | `ec.3d.fog3d` | 8 | GPU | 32 | Implemented |
|
||||
| ID Matte | `ec.3d.idmatte` | 5 | GPU | 32 | Implemented |
|
||||
| IDentifier | `ec.3d.identifier` | 3 | GPU | 32 | Implemented |
|
||||
|
||||
## Audio
|
||||
|
||||
|
||||
Reference in New Issue
Block a user