mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 22:20:00 -06:00
Merge main into M13.9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
//! Classic 3D depth of field on the GPU (`bokeh_*` kernels): the plane buffer padded, its
|
||||
//! highlights boosted, row prefix sums, then one span gather per blur level
|
||||
//! ([`effectcraft_render::three_d::bokeh::kernel_spans`]) accumulated with each pixel's level
|
||||
//! weight — the CPU's `progressive_blur` step for step.
|
||||
|
||||
use effectcraft_render::three_d::PlaneDof;
|
||||
use effectcraft_render::three_d::bokeh::kernel_spans;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
|
||||
/// Blur `src` (an unpadded plane buffer) as `d` says. Returns the padded, blurred image.
|
||||
pub(crate) fn apply(e: &mut Enc, src: &GpuImage, d: &PlaneDof) -> Option<GpuImage> {
|
||||
let (w, h) = (src.width + 2 * d.pad, src.height + 2 * d.pad);
|
||||
if !e.g.fits(w + 1, h) || d.radius.len() != w as usize * h as usize || d.levels.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let padded = e.image(w, h);
|
||||
e.copy_into(src, &padded, d.pad, d.pad);
|
||||
let boosted = if d.highlight.gain > 0.0 {
|
||||
let out = e.image(w, h);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [d.highlight.gain as f32, d.highlight.threshold as f32, d.highlight.saturation as f32, 0.0];
|
||||
e.pixels("bokeh_boost", &p, &padded, None, &out, None);
|
||||
out
|
||||
} else {
|
||||
padded
|
||||
};
|
||||
let prefix = e.image(w + 1, h);
|
||||
e.dispatch("bokeh_prefix", &Params::default(), &boosted, None, &prefix, None, (h.div_ceil(64), 1));
|
||||
// Radius map, then every level's spans.
|
||||
let mut data: Vec<f32> = d.radius.as_ref().clone();
|
||||
let mut levels = vec![];
|
||||
for &r in &d.levels {
|
||||
let (kernels, norm) = kernel_spans(&d.iris, r as f64).unwrap_or_else(|| (vec![(1.0, vec![(0, -0.5, 0.5)])], 1.0));
|
||||
let off = data.len() as u32;
|
||||
let mut count = 0u32;
|
||||
for (wt, spans) in &kernels {
|
||||
for &(dy, x0, x1) in spans {
|
||||
data.extend_from_slice(&[dy as f32, x0 as f32, x1 as f32, *wt]);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
levels.push((off, count, norm));
|
||||
}
|
||||
let buf = e.data(&data);
|
||||
let n = d.levels.len() as u32;
|
||||
let (lo, hi) = (d.levels[0], d.levels[d.levels.len() - 1]);
|
||||
let mut acc = e.image(w, h);
|
||||
for (k, (off, count, norm)) in levels.into_iter().enumerate() {
|
||||
let out = e.image(w, h);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [off, count, k as u32, n];
|
||||
p.u[1] = [u32::from(k == 0), 0, 0, 0];
|
||||
p.f[0] = [1.0 / norm, lo, hi, 0.0];
|
||||
e.pixels("bokeh_gather", &p, &prefix, Some(&acc), &out, Some(&buf));
|
||||
acc = out;
|
||||
}
|
||||
Some(acc)
|
||||
}
|
||||
@@ -136,11 +136,18 @@ pub(crate) fn draw_run(e: &mut Enc, run: &Run3d, canvas: &GpuImage) -> Option<Gp
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Plane textures: the layer uploads (cached per buffer; bokeh depth of field is already in
|
||||
// them).
|
||||
// Plane textures: the layer uploads (cached per buffer), with bokeh depth of field blurred
|
||||
// here when the plane has it.
|
||||
let mut texs: Vec<(GpuImage, [f64; 2])> = Vec::with_capacity(run.planes.len());
|
||||
for p in &run.planes {
|
||||
texs.push((e.g.upload_buf(&p.buf)?, p.buf.offset));
|
||||
let up = e.g.upload_buf(&p.buf)?;
|
||||
match &p.dof {
|
||||
Some(d) => {
|
||||
let pad = d.pad as f64;
|
||||
texs.push((crate::bokeh::apply(e, &up, d)?, [p.buf.offset[0] + pad, p.buf.offset[1] + pad]));
|
||||
}
|
||||
None => texs.push((up, p.buf.offset)),
|
||||
}
|
||||
}
|
||||
let sizes: Vec<(u32, u32)> = texs.iter().map(|(t, _)| (t.width, t.height)).collect();
|
||||
let (aw, ah, pos) = pack(&sizes, e.g.max_dim)?;
|
||||
|
||||
@@ -27,6 +27,9 @@ const ENTRIES: &[&str] = &[
|
||||
"shadow_combine",
|
||||
"pointwise",
|
||||
"adjust_mix",
|
||||
"bokeh_boost",
|
||||
"bokeh_prefix",
|
||||
"bokeh_gather",
|
||||
];
|
||||
|
||||
/// Entry points that also bind group 1 (four read-only storage buffers; see
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
//! egui-wgpu to draw.
|
||||
|
||||
mod adv3d;
|
||||
mod bokeh;
|
||||
mod classic3d;
|
||||
mod context;
|
||||
mod effects;
|
||||
|
||||
@@ -844,3 +844,118 @@ fn adjust_mix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let k = data[(gid.y * P.u[0].x + gid.x) * 4u + 3u] * P.f[0].x;
|
||||
textureStore(out, p, c + (a - c) * k);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Classic 3D bokeh depth of field
|
||||
// (render::three_d::bokeh: boost_highlights, row prefix sums, span gathers per blur level
|
||||
// blended by each pixel's radius.)
|
||||
|
||||
// Highlight boost: f[0] = (gain, threshold, saturation).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn bokeh_boost(@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;
|
||||
}
|
||||
var c = textureLoad(src, p, 0);
|
||||
let gain = P.f[0].x;
|
||||
let thr = clamp(P.f[0].y, 0.0, 1.0);
|
||||
let satk = P.f[0].z;
|
||||
if (c.w > 0.0 && gain > 0.0) {
|
||||
let a = c.w;
|
||||
let rgb = c.xyz / a;
|
||||
let l = 0.2126 * rgb.x + 0.7152 * rgb.y + 0.0722 * rgb.z;
|
||||
if (!(l < thr || (thr >= 1.0 && l < 1.0))) {
|
||||
var over = 1.0;
|
||||
if (thr < 1.0) {
|
||||
over = clamp((l - thr) / (1.0 - thr), 0.0, 1.0);
|
||||
}
|
||||
let k = 1.0 + gain * 4.0 * max(over, 0.25);
|
||||
let sat = 1.0 + satk;
|
||||
let o = max(vec3<f32>(l) + (rgb - vec3<f32>(l)) * sat, vec3<f32>(0.0)) * k;
|
||||
c = vec4<f32>(o * a, a);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, c);
|
||||
}
|
||||
|
||||
// Row prefix sums: out is (w + 1) × h, out[x] = sum of src[0..x]. One invocation per row.
|
||||
@compute @workgroup_size(64, 1)
|
||||
fn bokeh_prefix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = vec2<i32>(textureDimensions(src));
|
||||
let y = i32(gid.x);
|
||||
if (y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
var acc = vec4<f32>(0.0);
|
||||
textureStore(out, vec2<i32>(0, y), acc);
|
||||
for (var x = 0; x < dims.x; x = x + 1) {
|
||||
acc = acc + textureLoad(src, vec2<i32>(x, y), 0);
|
||||
textureStore(out, vec2<i32>(x + 1, y), acc);
|
||||
}
|
||||
}
|
||||
|
||||
fn bokeh_prefix_at(y: i32, t: f32) -> vec4<f32> {
|
||||
let n = i32(textureDimensions(src).x) - 1;
|
||||
if (t <= 0.0) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
if (t >= f32(n)) {
|
||||
return textureLoad(src, vec2<i32>(n, y), 0);
|
||||
}
|
||||
let i = i32(floor(t));
|
||||
let f = t - f32(i);
|
||||
let a = textureLoad(src, vec2<i32>(i, y), 0);
|
||||
let b = textureLoad(src, vec2<i32>(i + 1, y), 0);
|
||||
return a + (b - a) * f;
|
||||
}
|
||||
|
||||
// One blur level added into the accumulator `aux`: src = prefix sums; data = the radius map
|
||||
// (w·h) then spans (dy, x0, x1, weight). u[0] = (span offset, span count, level, levels),
|
||||
// u[1].x = 1 for the first level (no accumulator yet); f[0] = (1 / norm, lo, hi).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn bokeh_gather(@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;
|
||||
}
|
||||
var acc = vec4<f32>(0.0);
|
||||
if (P.u[1].x == 0u) {
|
||||
acc = textureLoad(aux, p, 0);
|
||||
}
|
||||
let r = data[u32(p.y) * u32(dims.x) + u32(p.x)];
|
||||
let k = P.u[0].z;
|
||||
let n = P.u[0].w;
|
||||
var wgt = 1.0;
|
||||
if (n > 1u) {
|
||||
let lo = P.f[0].y;
|
||||
let hi = P.f[0].z;
|
||||
let f = clamp((r - lo) / (hi - lo) * f32(n - 1u), 0.0, f32(n - 1u));
|
||||
let kk = min(u32(floor(f)), n - 2u);
|
||||
let t = f - f32(kk);
|
||||
if (k == kk) {
|
||||
wgt = 1.0 - t;
|
||||
} else if (k == kk + 1u) {
|
||||
wgt = t;
|
||||
} else {
|
||||
wgt = 0.0;
|
||||
}
|
||||
}
|
||||
if (wgt > 0.0) {
|
||||
var g = vec4<f32>(0.0);
|
||||
let off = P.u[0].x;
|
||||
for (var i = 0u; i < P.u[0].y; i = i + 1u) {
|
||||
let s = off + i * 4u;
|
||||
let sy = p.y + i32(data[s]);
|
||||
if (sy < 0 || sy >= dims.y) {
|
||||
continue;
|
||||
}
|
||||
let hi = bokeh_prefix_at(sy, f32(p.x) + data[s + 2u] + 0.5);
|
||||
let lo = bokeh_prefix_at(sy, f32(p.x) + data[s + 1u] + 0.5);
|
||||
g = g + (hi - lo) * data[s + 3u];
|
||||
}
|
||||
acc = acc + g * P.f[0].x * wgt;
|
||||
}
|
||||
textureStore(out, p, acc);
|
||||
}
|
||||
|
||||
@@ -218,3 +218,73 @@ fn the_gpu_draws_classic_3d_runs_itself() {
|
||||
let d = crate::tests::diff(&cpu, &img, 1e-3);
|
||||
assert!(d.over * 200 <= d.total, "{d:?}");
|
||||
}
|
||||
|
||||
/// Classic 3D bokeh depth of field runs on the GPU: iris shapes, highlights, fringe, Fast
|
||||
/// Rectangle and a tilted plane (progressive blur) match the CPU.
|
||||
#[test]
|
||||
fn bokeh_depth_of_field_runs_on_the_gpu() {
|
||||
let cases: [(&str, &[(&str, Value)]); 4] = [
|
||||
("fast rectangle", &[]),
|
||||
("hexagon + highlights", &[("irisShape", Value::Enum(4)), ("irisRotation", n(15.0)), ("highlightGain", n(40.0)), ("highlightThreshold", n(150.0))]),
|
||||
("round + fringe", &[("irisShape", Value::Enum(6)), ("irisRoundness", n(60.0)), ("irisDiffractionFringe", n(80.0))]),
|
||||
("wide triangle", &[("irisShape", Value::Enum(1)), ("irisAspectRatio", n(2.0))]),
|
||||
];
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
for (label, vals) in cases {
|
||||
// The crossing planes are tilted (±35°): every plane blurs progressively.
|
||||
let mut s = crossing(depth, BlendMode::Normal);
|
||||
camera(&mut s, [80.0, 50.0, -220.0], Some((180.0, 40.0)));
|
||||
for l in &mut s.p.comp_mut(s.cid).unwrap().layers {
|
||||
if l.name == "Camera" {
|
||||
for (k, v) in vals {
|
||||
set(l, &format!("cameraOptions/{k}"), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
check(&format!("bokeh {label} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
// The blur is the GPU's: the prepared planes carry it unapplied.
|
||||
if depth == BitDepth::Bpc32
|
||||
&& let Some(g) = gpu()
|
||||
{
|
||||
let r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
let ctx = r.eval_ctx(s.cid, Tick::ZERO).unwrap();
|
||||
let run: Vec<_> = r.visible_layers(&ctx).into_iter().filter(|l| l.is_3d()).collect();
|
||||
let prep = r.prepare_3d_run(&ctx, &run, (W, H)).unwrap();
|
||||
assert!(prep.planes.iter().filter(|p| p.draw).all(|p| p.dof.is_some()), "{label}: depth of field deferred to the GPU");
|
||||
let mut e = crate::context::Enc::new(g.context());
|
||||
let canvas = e.image(W, H);
|
||||
assert!(crate::classic3d::draw_run(&mut e, &prep, &canvas).is_some(), "{label}: drawn on the GPU");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A 2D layer whose track matte is a 3D layer: the matte is drawn through the camera on the
|
||||
/// GPU walk as on the CPU.
|
||||
#[test]
|
||||
fn two_d_layer_with_a_three_d_track_matte() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
for kind in [MatteKind::Alpha, MatteKind::Luma] {
|
||||
let mut s = scene(depth);
|
||||
let bg = s.solid([0.1, 0.12, 0.2], W, H);
|
||||
s.push(bg);
|
||||
let fill = s.footage(W, H);
|
||||
let fid = s.push(fill);
|
||||
// The matte: a solid turned in 3D, seen through a camera.
|
||||
let m = three_d(s.solid([0.9, 0.9, 0.9], 70, 50), [80.0, 50.0, 30.0], 40.0);
|
||||
let mid = s.push(m);
|
||||
camera(&mut s, [40.0, 30.0, -200.0], None);
|
||||
let layers = &mut s.p.comp_mut(s.cid).unwrap().layers;
|
||||
let fi = layers.iter().position(|l| l.id == fid).unwrap();
|
||||
layers[fi].track_matte = Some(TrackMatte { layer: mid, kind });
|
||||
if let Some(ml) = layers.iter_mut().find(|l| l.id == mid) {
|
||||
ml.switches.video = false;
|
||||
}
|
||||
check(&format!("3d matte {kind:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
// The same with the Advanced 3D renderer (its CPU path draws 3D mattes through the
|
||||
// camera with composite_iso_with).
|
||||
s.p.comp_mut(s.cid).unwrap().renderer = effectcraft_project::Renderer::Advanced3D;
|
||||
check(&format!("3d matte {kind:?} {depth:?} (Advanced 3D)"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user