effectcraft/crates/gpu/src/fx_depth.rs

336 lines
15 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.

//! 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 })
}