mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 12:07:48 -06:00
M13.6: Classic 3D bokeh depth of field on the GPU (WGSL); 3D-matte GPU check
Planes prepared for the GPU carry their depth of field (padding, radius map, levels) and the bokeh_boost / bokeh_prefix / bokeh_gather kernels blur them where they are drawn, with the CPU's spans; GPU-vs-CPU agreement tests for iris shapes, highlights, fringe, Fast Rectangle, tilted planes and a 2D layer with a 3D track matte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f89af341a6
commit
a2693c9d15
@@ -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,69 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,8 @@ pub struct Renderer<'a> {
|
||||
/// Switches inherited from the precomp layers this comp is rendered through (see
|
||||
/// [`RenderOpts::nested_switches`]): the worst quality and whether motion blur is allowed.
|
||||
pub(crate) inherited: Option<(Quality, bool)>,
|
||||
/// Classic 3D depth of field is left to an accelerator ([`three_d::Plane3d::dof`]).
|
||||
pub(crate) defer_dof: bool,
|
||||
}
|
||||
|
||||
/// How a collapsed precomp's 3D layers are placed in the parent (see `Renderer::collapse_into`).
|
||||
@@ -407,6 +409,7 @@ impl<'a> Renderer<'a> {
|
||||
opacity_mul: 1.0,
|
||||
collapse3d: None,
|
||||
inherited: None,
|
||||
defer_dof: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +444,7 @@ impl<'a> Renderer<'a> {
|
||||
|
||||
/// A renderer for nested work (precomps, layers read by effects): one level deeper.
|
||||
fn nested(&self) -> Renderer<'a> {
|
||||
Renderer { depth: self.depth + 1, outer: None, opacity_mul: 1.0, collapse3d: None, ..*self }
|
||||
Renderer { depth: self.depth + 1, outer: None, opacity_mul: 1.0, collapse3d: None, defer_dof: false, ..*self }
|
||||
}
|
||||
|
||||
/// The nested comp of a precomp layer whose transformations collapse into this comp: the
|
||||
|
||||
@@ -190,21 +190,28 @@ pub fn boost_highlights(img: &mut Image, h: &Highlight) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Gather blur of a premultiplied image with a bokeh kernel of radius `r` (pixels). Pixels
|
||||
/// beyond the image are transparent.
|
||||
pub fn bokeh_blur(img: &Image, iris: &Iris, r: f64) -> Image {
|
||||
/// Weighted row-span kernels and their normalisation.
|
||||
pub type Kernels = (Vec<(f32, Vec<(i32, f64, f64)>)>, f32);
|
||||
|
||||
/// The bokeh kernel of radius `r` as weighted row spans (see [`Iris::spans`]) and the sum of
|
||||
/// weights; `None` when the blur does nothing (radius below 0.3 px). Fast Rectangle is the box
|
||||
/// of the iris' width and height (one span per row covering whole pixels); the diffraction
|
||||
/// fringe adds the rim as the full iris minus a shrunken one. The GPU gathers with exactly these
|
||||
/// spans.
|
||||
pub fn kernel_spans(iris: &Iris, r: f64) -> Option<Kernels> {
|
||||
if r < 0.3 {
|
||||
return img.clone();
|
||||
return None;
|
||||
}
|
||||
if iris.sides == 0 {
|
||||
// Fast Rectangle: a separable box of the iris' width and height.
|
||||
let a = iris.aspect.clamp(0.01, 100.0).sqrt();
|
||||
let rx = (r * a).round().max(0.0) as usize;
|
||||
let ry = (r / a).round().max(0.0) as usize;
|
||||
return effectcraft_raster::box_blur(img, rx, ry, 1, false);
|
||||
let rx = (r * a).round().max(0.0) as i32;
|
||||
let ry = (r / a).round().max(0.0) as i32;
|
||||
if rx == 0 && ry == 0 {
|
||||
return None;
|
||||
}
|
||||
let spans = (-ry..=ry).map(|dy| (dy, -rx as f64 - 0.5, rx as f64 + 0.5)).collect();
|
||||
return Some((vec![(1.0, spans)], ((2 * rx + 1) * (2 * ry + 1)) as f32));
|
||||
}
|
||||
// The iris as row spans (prefix-sum gather: cost ∝ kernel height, not area). The
|
||||
// diffraction fringe adds the rim: the full iris minus a shrunken one.
|
||||
let outer = iris.spans(r);
|
||||
let area = |sp: &[(i32, f64, f64)]| sp.iter().map(|s| (s.2 - s.1) as f32).sum::<f32>();
|
||||
let mut kernels = vec![(1.0f32, outer)];
|
||||
@@ -216,25 +223,72 @@ pub fn bokeh_blur(img: &Image, iris: &Iris, r: f64) -> Image {
|
||||
kernels.push((-f, inner));
|
||||
}
|
||||
let norm: f32 = kernels.iter().map(|(wt, sp)| wt * area(sp)).sum();
|
||||
if norm <= 0.0 {
|
||||
(norm > 0.0).then_some((kernels, norm))
|
||||
}
|
||||
|
||||
/// Gather blur of a premultiplied image with a bokeh kernel of radius `r` (pixels). Pixels
|
||||
/// beyond the image are transparent.
|
||||
pub fn bokeh_blur(img: &Image, iris: &Iris, r: f64) -> Image {
|
||||
if r < 0.3 {
|
||||
return img.clone();
|
||||
}
|
||||
span_gather(img, &kernels, norm)
|
||||
if iris.sides == 0 {
|
||||
// Fast Rectangle: a separable box of the iris' width and height (the same as its spans).
|
||||
let a = iris.aspect.clamp(0.01, 100.0).sqrt();
|
||||
let rx = (r * a).round().max(0.0) as usize;
|
||||
let ry = (r / a).round().max(0.0) as usize;
|
||||
return effectcraft_raster::box_blur(img, rx, ry, 1, false);
|
||||
}
|
||||
// The iris as row spans (prefix-sum gather: cost ∝ kernel height, not area).
|
||||
match kernel_spans(iris, r) {
|
||||
Some((kernels, norm)) => span_gather(img, &kernels, norm),
|
||||
None => img.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The blur levels of a depth-varying blur ([`progressive_blur`]): `None` when nothing blurs,
|
||||
/// one level for a near-constant radius, else up to eight radii spanning the range.
|
||||
pub fn blur_levels(radius: &[f32]) -> Option<Vec<f32>> {
|
||||
let (lo, hi) = radius.iter().fold((f32::INFINITY, 0.0f32), |(a, b), &r| (a.min(r), b.max(r)));
|
||||
if !lo.is_finite() || hi < 0.3 {
|
||||
return None;
|
||||
}
|
||||
if hi - lo < 0.75 {
|
||||
return Some(vec![(lo + hi) * 0.5]);
|
||||
}
|
||||
let n = (((hi - lo) / 2.0).ceil() as usize + 1).clamp(2, 8);
|
||||
Some((0..n).map(|i| lo + (hi - lo) * i as f32 / (n - 1) as f32).collect())
|
||||
}
|
||||
|
||||
/// How much level `k` of `levels` contributes at radius `r` (the two levels bracketing `r`
|
||||
/// are blended linearly).
|
||||
pub fn level_weight(levels: &[f32], k: usize, r: f32) -> f32 {
|
||||
let n = levels.len();
|
||||
if n <= 1 {
|
||||
return 1.0;
|
||||
}
|
||||
let (lo, hi) = (levels[0], levels[n - 1]);
|
||||
let f = ((r - lo) / (hi - lo) * (n - 1) as f32).clamp(0.0, (n - 1) as f32);
|
||||
let kk = (f.floor() as usize).min(n - 2);
|
||||
let t = f - kk as f32;
|
||||
if k == kk {
|
||||
1.0 - t
|
||||
} else if k == kk + 1 {
|
||||
t
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Depth-varying blur: `radius` gives each pixel's blur radius. The image is blurred at up to
|
||||
/// eight radii spanning the range and each pixel interpolates between the two levels around
|
||||
/// its own radius.
|
||||
pub fn progressive_blur(img: &Image, iris: &Iris, radius: &[f32]) -> Image {
|
||||
let (lo, hi) = radius.iter().fold((f32::INFINITY, 0.0f32), |(a, b), &r| (a.min(r), b.max(r)));
|
||||
if !lo.is_finite() || hi < 0.3 {
|
||||
return img.clone();
|
||||
let Some(levels) = blur_levels(radius) else { return img.clone() };
|
||||
if levels.len() == 1 {
|
||||
return bokeh_blur(img, iris, levels[0] as f64);
|
||||
}
|
||||
if hi - lo < 0.75 {
|
||||
return bokeh_blur(img, iris, ((lo + hi) * 0.5) as f64);
|
||||
}
|
||||
let n = (((hi - lo) / 2.0).ceil() as usize + 1).clamp(2, 8);
|
||||
let levels: Vec<f32> = (0..n).map(|i| lo + (hi - lo) * i as f32 / (n - 1) as f32).collect();
|
||||
let (lo, hi, n) = (levels[0], levels[levels.len() - 1], levels.len());
|
||||
let blurred: Vec<Image> = levels.par_iter().map(|&r| bokeh_blur(img, iris, r as f64)).collect();
|
||||
let mut out = Image::new(img.width, img.height);
|
||||
out.data.par_iter_mut().enumerate().for_each(|(i, o)| {
|
||||
|
||||
@@ -62,6 +62,21 @@ pub(crate) struct Item<'a> {
|
||||
winv: Mat4,
|
||||
bounds: [f64; 4],
|
||||
bbox: [i64; 4],
|
||||
/// Depth of field left to an accelerator (`buf` is then unblurred and unpadded).
|
||||
dof: Option<PlaneDof>,
|
||||
}
|
||||
|
||||
/// Depth of field of a plane for an accelerator to apply ([`Plane3d::dof`]): pad the buffer by
|
||||
/// `pad` transparent pixels, boost its highlights, then blur it at each of `levels` with
|
||||
/// [`super::bokeh::kernel_spans`] and blend the levels by each pixel's `radius` (padded buffer
|
||||
/// pixels, row-major) with [`super::bokeh::level_weight`] — what the CPU's depth of field does.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlaneDof {
|
||||
pub pad: u32,
|
||||
pub iris: super::bokeh::Iris,
|
||||
pub highlight: super::bokeh::Highlight,
|
||||
pub radius: Arc<Vec<f32>>,
|
||||
pub levels: Vec<f32>,
|
||||
}
|
||||
|
||||
impl Item<'_> {
|
||||
@@ -216,10 +231,20 @@ fn prepare_with<'a>(
|
||||
let world = outer * ctx.world_matrix(layer) * local;
|
||||
// Depth of field: a bokeh blur by each buffer pixel's circle of confusion (constant for a
|
||||
// layer facing the camera, progressive for a tilted one).
|
||||
let mut plane_dof = None;
|
||||
let mut bounds = buf_bounds(&buf);
|
||||
if let Some(dof) = cam.dof.filter(|_| !ctx.comp.draft_3d && !r.opts.draft) {
|
||||
buf = dof_blur(&cam, &dof, world, buf);
|
||||
if r.defer_dof {
|
||||
// An accelerator blurs: hand it the radius map of the padded buffer.
|
||||
if let Some((pd, padded_bounds)) = dof_plan(&cam, &dof, world, &buf) {
|
||||
plane_dof = Some(pd);
|
||||
bounds = padded_bounds;
|
||||
}
|
||||
} else {
|
||||
buf = dof_blur(&cam, &dof, world, buf);
|
||||
bounds = buf_bounds(&buf);
|
||||
}
|
||||
}
|
||||
let bounds = buf_bounds(&buf);
|
||||
let n = if in_run { mb_samples(r, ctx, layer) } else { 1 };
|
||||
let geos: Vec<Geo> = if n <= 1 {
|
||||
geo(&cam, world, bounds, comp, s, out).into_iter().collect()
|
||||
@@ -258,16 +283,20 @@ fn prepare_with<'a>(
|
||||
winv: world.inverse().unwrap_or(Mat4::IDENTITY),
|
||||
bounds,
|
||||
bbox,
|
||||
dof: plane_dof,
|
||||
})
|
||||
}
|
||||
|
||||
/// Blur radius (buffer pixels) of every buffer pixel of a plane at `world` (layer → world), from
|
||||
/// its camera depth: the circle of confusion over the on-screen size of one layer pixel there.
|
||||
fn dof_radii(cam: &CameraState, dof: &super::camera::Dof, world: Mat4, buf: &Buf) -> Vec<f32> {
|
||||
let s = buf.scale.max(1e-9);
|
||||
dof_radii_at(cam, dof, world, (buf.img.width as usize, buf.img.height as usize), buf.offset, buf.scale)
|
||||
}
|
||||
|
||||
fn dof_radii_at(cam: &CameraState, dof: &super::camera::Dof, world: Mat4, (w, h): (usize, usize), offset: [f64; 2], scale: f64) -> Vec<f32> {
|
||||
let s = scale.max(1e-9);
|
||||
let sx = world.apply_vec(vec3(1.0, 0.0, 0.0)).length().max(1e-9);
|
||||
let (w, h) = (buf.img.width as usize, buf.img.height as usize);
|
||||
let at = |x: f64, y: f64| cam.depth(world.apply(vec3((x - buf.offset[0]) / s, (y - buf.offset[1]) / s, 0.0)));
|
||||
let at = |x: f64, y: f64| cam.depth(world.apply(vec3((x - offset[0]) / s, (y - offset[1]) / s, 0.0)));
|
||||
// Camera depth is affine across a plane.
|
||||
let z0 = at(0.5, 0.5);
|
||||
let (zx, zy) = (at(1.5, 0.5) - z0, at(0.5, 1.5) - z0);
|
||||
@@ -285,6 +314,30 @@ fn dof_radii(cam: &CameraState, dof: &super::camera::Dof, world: Mat4, buf: &Buf
|
||||
out
|
||||
}
|
||||
|
||||
/// The padding [`dof_blur`] gives a buffer whose largest blur radius is `rmax`.
|
||||
fn dof_pad(dof: &super::camera::Dof, rmax: f64) -> u32 {
|
||||
let a = dof.iris.aspect.clamp(0.01, 100.0).sqrt().max(1.0 / dof.iris.aspect.clamp(0.01, 100.0).sqrt());
|
||||
(rmax * a).ceil() as u32 + 2
|
||||
}
|
||||
|
||||
/// [`dof_blur`] planned for an accelerator: the padding, radius map and levels, and the padded
|
||||
/// buffer's layer-space bounds. `None` when nothing blurs.
|
||||
fn dof_plan(cam: &CameraState, dof: &super::camera::Dof, world: Mat4, buf: &Buf) -> Option<(PlaneDof, [f64; 4])> {
|
||||
let radii = dof_radii(cam, dof, world, buf);
|
||||
let rmax = radii.iter().copied().fold(0.0f32, f32::max) as f64;
|
||||
if rmax <= 0.3 {
|
||||
return None;
|
||||
}
|
||||
let pad = dof_pad(dof, rmax);
|
||||
let (w, h) = (buf.img.width as usize + 2 * pad as usize, buf.img.height as usize + 2 * pad as usize);
|
||||
let offset = [buf.offset[0] + pad as f64, buf.offset[1] + pad as f64];
|
||||
let radius = dof_radii_at(cam, dof, world, (w, h), offset, buf.scale);
|
||||
let levels = super::bokeh::blur_levels(&radius)?;
|
||||
let s = buf.scale.max(1e-9);
|
||||
let bounds = [-offset[0] / s, -offset[1] / s, (w as f64 - offset[0]) / s, (h as f64 - offset[1]) / s];
|
||||
Some((PlaneDof { pad, iris: dof.iris, highlight: dof.highlight, radius: Arc::new(radius), levels }, bounds))
|
||||
}
|
||||
|
||||
/// Depth of field for one plane: highlights boosted, then a bokeh-shaped blur by depth.
|
||||
fn dof_blur(cam: &CameraState, dof: &super::camera::Dof, world: Mat4, buf: Arc<Buf>) -> Arc<Buf> {
|
||||
let radii = dof_radii(cam, dof, world, &buf);
|
||||
@@ -292,8 +345,7 @@ fn dof_blur(cam: &CameraState, dof: &super::camera::Dof, world: Mat4, buf: Arc<B
|
||||
if rmax <= 0.3 {
|
||||
return buf;
|
||||
}
|
||||
let a = dof.iris.aspect.clamp(0.01, 100.0).sqrt().max(1.0 / dof.iris.aspect.clamp(0.01, 100.0).sqrt());
|
||||
let pad = (rmax * a).ceil() as u32 + 2;
|
||||
let pad = dof_pad(dof, rmax);
|
||||
let mut padded = buf.img.padded(pad);
|
||||
super::bokeh::boost_highlights(&mut padded, &dof.highlight);
|
||||
let pbuf = Buf { img: padded, offset: [buf.offset[0] + pad as f64, buf.offset[1] + pad as f64], scale: buf.scale };
|
||||
@@ -664,8 +716,11 @@ pub struct Run3d {
|
||||
/// One plane of a [`Run3d`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Plane3d {
|
||||
/// The layer buffer (blending space; depth of field already applied).
|
||||
/// The layer buffer (blending space; depth of field already applied unless `dof` is set).
|
||||
pub buf: Arc<Buf>,
|
||||
/// Depth of field for the accelerator to apply to `buf` (`bounds` are then the padded
|
||||
/// buffer's).
|
||||
pub dof: Option<PlaneDof>,
|
||||
pub bicubic: bool,
|
||||
/// One per motion-blur sub-sample (equal weights).
|
||||
pub geos: Vec<Geo>,
|
||||
@@ -695,7 +750,9 @@ pub(crate) fn gpu_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], out: (u32, u3
|
||||
if ctx.comp.renderer == effectcraft_project::Renderer::Advanced3D && r.collapse3d.is_none() {
|
||||
return None;
|
||||
}
|
||||
let g = gather(r, ctx, run, out);
|
||||
// Depth of field is left to the accelerator (its blur runs where the planes are drawn).
|
||||
let rd = Renderer { defer_dof: true, ..*r };
|
||||
let g = gather(&rd, ctx, run, out);
|
||||
let planes: Vec<Plane3d> = g
|
||||
.items
|
||||
.into_iter()
|
||||
@@ -704,6 +761,7 @@ pub(crate) fn gpu_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], out: (u32, u3
|
||||
layer_id: it.layer.id.0 as u32,
|
||||
mode: it.layer.blend_mode,
|
||||
buf: it.buf,
|
||||
dof: it.dof,
|
||||
bicubic: it.bicubic,
|
||||
geos: it.geos,
|
||||
mat: it.mat,
|
||||
|
||||
@@ -17,7 +17,7 @@ mod tests;
|
||||
mod tests_orient;
|
||||
|
||||
pub use camera::{CameraState, Dof, Rig, View3D, ViewCam, Views3D, active_camera, default_camera, default_view_cam};
|
||||
pub use compose::{Geo as PlaneGeo, Plane3d, Run3d};
|
||||
pub use compose::{Geo as PlaneGeo, Plane3d, PlaneDof, Run3d};
|
||||
pub use light::{LightState, Material, lights_at};
|
||||
|
||||
use effectcraft_geom::Mat3;
|
||||
|
||||
Reference in New Issue
Block a user