Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions crates/compositor/src/compositor_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1976,7 +1976,7 @@ impl Compositor {
color: [1.0, 1.0, 1.0, 1.0],
src_prev: g.cut,
dst_prev: g.s_dst_prev,
mb: [g.mb_taps, 1.0, 1.0, 0.0],
mb: [g.mb_taps, g.mb_amount, 1.0, 0.0],
..Default::default()
},
Some(quad) => self.tilted_screen_cb(quad, s_px, quad_center_px, g.cut, g.s_radius),
Expand Down Expand Up @@ -2146,7 +2146,7 @@ impl Compositor {
fx: [w_valid[0], w_valid[1], effect_code, blur_intensity],
src_prev: [u0, cv0, u1, cv1],
dst_prev: g.w_dst_prev,
mb: [g.mb_taps, 1.0, 1.0, 0.0],
mb: [g.mb_taps, g.mb_amount, 1.0, 0.0],
..Default::default()
};
// Le masque est lie par `make_bind` sur tous les draws, pas seulement
Expand Down Expand Up @@ -2856,9 +2856,9 @@ impl Compositor {
occlusion_query_set: None,
});
rpass.set_pipeline(&self.pipeline_add);
let w = 1.0 / c.binds.len() as f64;
rpass.set_blend_constant(wgpu::Color { r: w, g: w, b: w, a: w });
for bind in &c.binds {
for (k, bind) in c.binds.iter().enumerate() {
let w = crate::frame_geometry::cursor_tap_weight(k as u32, c.binds.len() as u32) as f64;
rpass.set_blend_constant(wgpu::Color { r: w, g: w, b: w, a: w });
rpass.set_bind_group(0, bind, &[]);
rpass.draw(0..4, 0..1);
}
Expand Down
8 changes: 4 additions & 4 deletions crates/compositor/src/compositor_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2102,7 +2102,7 @@ impl Compositor {
color: [0.0, 0.0, 0.0, 1.0],
src_prev: [su0, sv0, su1, sv1],
dst_prev: g.s_dst_prev,
mb: [g.mb_taps, 1.0, 1.0, 0.0],
mb: [g.mb_taps, g.mb_amount, 1.0, 0.0],
..Default::default()
},
&sy,
Expand Down Expand Up @@ -2151,10 +2151,10 @@ impl Compositor {
Some(metal::MTLClearColor::new(0.0, 0.0, 0.0, 0.0)),
&self.pipeline_add,
)?;
let w = 1.0 / plan.taps as f32;
e.set_blend_color(w, w, w, w);
for k in 0..plan.taps {
let f = k as f32 / (plan.taps - 1) as f32;
let w = crate::frame_geometry::cursor_tap_weight(k, plan.taps);
e.set_blend_color(w, w, w, w);
self.draw_cur_themed(
e,
&sprites,
Expand Down Expand Up @@ -2261,7 +2261,7 @@ impl Compositor {
fx: [w_valid[0], w_valid[1], effect_code, blur_intensity],
src_prev: [u0, cv0, u1, cv1],
dst_prev: g.w_dst_prev,
mb: [g.mb_taps, 1.0, 1.0, 0.0],
mb: [g.mb_taps, g.mb_amount, 1.0, 0.0],
..Default::default()
},
wy,
Expand Down
252 changes: 79 additions & 173 deletions crates/compositor/src/compositor_windows.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/compositor/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl CursorTrack {
/// Seul point de construction : garantit que `follow_samples` est toujours dérivé des
/// échantillons courants. Une piste re-lissée (`smoothed`) recalcule donc aussi son suivi,
/// pour que la caméra suive la trajectoire que l'utilisateur voit réellement.
fn new(samples: Vec<(f32, f32, f32)>, clicks: Vec<f32>, types: Vec<(f32, String)>) -> CursorTrack {
pub(crate) fn new(samples: Vec<(f32, f32, f32)>, clicks: Vec<f32>, types: Vec<(f32, String)>) -> CursorTrack {
let follow_samples = smooth_follow_samples(&samples);
CursorTrack { samples, follow_samples, clicks, types }
}
Expand Down
162 changes: 152 additions & 10 deletions crates/compositor/src/frame_geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ pub(crate) fn cursor_sprite_dst(center: [f32; 2], w: f32, h: f32, hotspot: [f32;
/// donc pas seulement sa position qu'il faut projeter mais son sprite entier : autrement il se
/// lit comme un autocollant plat posé sur une scène en perspective.
#[derive(Clone, Copy)]
pub(crate) enum CursorPlacement {
pub enum CursorPlacement {
/// Écran droit : centre en coordonnées sortie 0..1.
Upright { center: [f32; 2] },
/// Écran incliné : position 0..1 DANS le plan, plus de quoi projeter les coins du sprite.
Expand Down Expand Up @@ -732,6 +732,7 @@ pub struct FrameGeometryInput<'a> {
pub struct FrameGeometry {
pub scene_preset: Option<String>,
pub mb_taps: f32,
pub mb_amount: f32,
pub source_t: f32,
pub zoom_rotation: [f32; 3],
pub padding_scale: f32,
Expand Down Expand Up @@ -862,6 +863,9 @@ pub fn plan_frame(input: &FrameGeometryInput) -> FrameGeometry {
let mb_taps = scene
.map(|s| 1.0 + s.effects.motion_blur.clamp(0.0, 1.0) * 15.0)
.unwrap_or(cfg.mblur_n as f32);
let mb_amount = scene
.map(|s| s.effects.motion_blur.clamp(0.0, 1.0))
.unwrap_or(if cfg.mblur_n > 1 { 1.0 } else { 0.0 });

// Zoom regions + Full Camera : filtrées en amont pour le clip actif et échantillonnées
// dans le même référentiel source que le PTS du décodeur écran.
Expand Down Expand Up @@ -1165,6 +1169,7 @@ pub fn plan_frame(input: &FrameGeometryInput) -> FrameGeometry {
FrameGeometry {
scene_preset,
mb_taps,
mb_amount,
source_t,
zoom_rotation,
padding_scale,
Expand Down Expand Up @@ -1280,16 +1285,32 @@ pub fn plan_cursor(g: &FrameGeometry, input: &CursorPlanInput) -> Option<CursorP

let blur01 = lp.cursor_motion_blur.clamp(0.0, 1.0);
let has_scene = input.scene.is_some();
let trail_frames = if has_scene { 1.0 + blur01 * 7.0 } else { 1.0 };
let taps = if has_scene {
(1.0 + blur01 * 10.0).round() as u32
} else {
input.cfg.mblur_n
};
let prev_placement = if taps <= 1 {
placement
let (taps, prev_placement) = if !has_scene {
let taps = input.cfg.mblur_n;
let prev = if taps <= 1 {
placement
} else {
place(input.track.at(input.t - 1.0 / FPS), g.s_dst_prev).unwrap_or(placement)
};
(taps, prev)
} else if blur01 <= 0.001 {
(1, placement)
} else {
place(input.track.at(input.t - trail_frames / FPS), g.s_dst_prev).unwrap_or(placement)
// Intervalle d'obturateur court, borné à 1 frame (100% blur = 1 frame d'exposition)
let trail_dt = blur01 / FPS;
let prev = place(input.track.at(input.t - trail_dt), g.s_dst_prev).unwrap_or(placement);
let c_now = placement.upright_center();
let c_prev = prev.upright_center();
let dist_px = ((c_now[0] - c_prev[0]) * rw).hypot((c_now[1] - c_prev[1]) * rh);
if dist_px < 1.0 {
// Quasi immobile : pas de copies superflues
(1, placement)
} else {
// Densité d'échantillonnage adaptée à la distance pour éliminer les fantômes discrets
let needed = (dist_px / 2.5).ceil() as u32;
let taps = needed.clamp(2, 16);
(taps, prev)
}
};

Some(CursorPlan {
Expand All @@ -1302,6 +1323,21 @@ pub fn plan_cursor(g: &FrameGeometry, input: &CursorPlanInput) -> Option<CursorP
})
}

/// Poids d'un échantillon du flou de mouvement de curseur (0 = queue/passé, taps-1 = tête/courant).
///
/// Poids croissant de 0.25 (queue) à 1.0 (tête), normalisé pour que la somme valle 1.0.
/// La somme des (0.25 + 0.75 * k / (taps - 1)) pour k de 0 à taps-1 vaut taps * (0.25 + 1.0) / 2 = taps * 0.625.
#[inline]
pub fn cursor_tap_weight(k: u32, taps: u32) -> f32 {
if taps <= 1 {
return 1.0;
}
let t = k as f32 / (taps - 1) as f32;
let ramp = 0.25 + 0.75 * t;
let sum = taps as f32 * 0.625;
ramp / sum
}

/// Les clés à évincer d'un cache de textures pour repasser sous `budget`, la moins récemment
/// utilisée d'abord. `entries` porte `(clé, octets, tick d'usage)`.
///
Expand Down Expand Up @@ -1983,4 +2019,110 @@ mod tests {
let uv = webcam_source_rect([100.0, 100.0], [100.0, 100.0], Some(crop), 0.50 / 0.60);
assert_rect(uv, [0.25, 0.20, 0.75, 0.80]);
}

#[test]
fn cursor_tap_weight_sums_to_one_and_is_monotonically_increasing() {
assert_eq!(cursor_tap_weight(0, 1), 1.0);

for taps in [2, 4, 8, 11, 16] {
let mut sum = 0.0;
let mut prev_w = 0.0;
for k in 0..taps {
let w = cursor_tap_weight(k, taps);
assert!(w > 0.0, "poids positif");
if k > 0 {
assert!(w > prev_w, "tête plus marquée que la queue : {w} > {prev_w}");
}
prev_w = w;
sum += w;
}
assert!((sum - 1.0).abs() < 1e-5, "somme des poids = 1.0 pour taps={taps}, got {sum}");
}
}

#[test]
fn plan_cursor_motion_blur_adaptive_and_stationary() {
let cfg = crate::config::all().pop().expect("cfg");
let track_immobile = crate::cursor::CursorTrack::new(
vec![(0.0, 0.5, 0.5), (2.0, 0.5, 0.5)],
vec![],
vec![],
);
let track_moving = crate::cursor::CursorTrack::new(
vec![(0.0, 0.1, 0.1), (1.0, 0.9, 0.9)],
vec![],
vec![],
);
let scene = zoomed_golden_scene();
let fg = FrameGeometry {
scene_preset: None,
mb_taps: 1.0,
mb_amount: 0.0,
source_t: 0.0,
zoom_rotation: [0.0, 0.0, 0.0],
padding_scale: 1.0,
cut: [0.0, 0.0, 1.0, 1.0],
s_dst: [0.0, 0.0, 1.0, 1.0],
s_dst_prev: [0.0, 0.0, 1.0, 1.0],
s_ann: [0.0, 0.0, 1.0, 1.0],
s_radius: 0.0,
frame_min_px: 1080.0,
w_dst: [0.0, 0.0, 0.0, 0.0],
w_dst_prev: [0.0, 0.0, 0.0, 0.0],
w_px: [0.0, 0.0],
w_radius: 0.0,
shape_fade: 0.0,
};

// 1. Curseur immobile avec blur actif -> taps = 1
let live_with_blur = LiveParams {
cursor_motion_blur: 0.8,
..LiveParams::default()
};
let input_immobile = CursorPlanInput {
render_px: [1920.0, 1080.0],
u_max: 1.0,
v_max: 1.0,
cfg: &cfg,
live: live_with_blur,
scene: Some(&scene),
track: &track_immobile,
t: 0.5,
};
let plan = plan_cursor(&fg, &input_immobile).expect("plan cursor");
assert_eq!(plan.taps, 1, "curseur immobile doit rester à 1 tap");

// 2. Curseur avec blur = 0 -> taps = 1
let live_no_blur = LiveParams {
cursor_motion_blur: 0.0,
..LiveParams::default()
};
let input_no_blur = CursorPlanInput {
render_px: [1920.0, 1080.0],
u_max: 1.0,
v_max: 1.0,
cfg: &cfg,
live: live_no_blur,
scene: Some(&scene),
track: &track_moving,
t: 0.5,
};
let plan = plan_cursor(&fg, &input_no_blur).expect("plan cursor");
assert_eq!(plan.taps, 1, "blur=0 doit donner taps = 1");

// 3. Curseur en mouvement rapide avec blur -> taps adaptatifs entre 2 et 16
let input_moving = CursorPlanInput {
render_px: [1920.0, 1080.0],
u_max: 1.0,
v_max: 1.0,
cfg: &cfg,
live: live_with_blur,
scene: Some(&scene),
track: &track_moving,
t: 0.5,
};
let plan = plan_cursor(&fg, &input_moving).expect("plan cursor");
assert!(plan.taps >= 2 && plan.taps <= 16, "taps adaptatifs dans [2, 16], got {}", plan.taps);
}
}

61 changes: 46 additions & 15 deletions crates/compositor/src/shaders.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -174,22 +174,51 @@ float3 quad_inverse_bilinear(float2 P, float2 c00, float2 c10, float2 c11, float
return (r0.z > 0.5) ? r0 : r1;
}

// Fond floute pour le mode "blur" de la webcam. Rayon en UV pour rester isotrope quel que soit
// le rect source. 25 taps ponderes par la distance : assez doux pour un fond, assez court pour
// tenir dans le budget d'une frame webcam (qui n'occupe qu'une fraction de la sortie).
float3 blur_webcam_bg(float2 uv, float intensity, float2 qpx)
// Fond flouté pour le mode "blur" de la webcam.
// Disque de Vogel (spirale à angle d'or) à 21 échantillons avec pondération gaussienne et
// rotation par pixel via Interleaved Gradient Noise (IGN) pour un bokeh photographique doux, isotrope et rapide.
static const float3 VOGEL_TAPS[21] = {
float3( 0.154303, 0.000000, 0.942213),
float3(-0.197070, 0.180532, 0.836464),
float3( 0.030165, -0.343712, 0.742584),
float3( 0.248394, 0.323986, 0.659241),
float3(-0.455834, -0.080631, 0.585251),
float3( 0.431806, -0.274679, 0.519566),
float3(-0.144431, 0.537274, 0.461253),
float3(-0.275445, -0.530352, 0.409484),
float3( 0.597605, 0.218244, 0.363526),
float3(-0.621708, 0.256632, 0.322726),
float3( 0.299704, -0.640451, 0.286505),
float3( 0.221474, 0.706094, 0.254349),
float3(-0.667525, -0.386844, 0.225802),
float3( 0.783083, -0.172159, 0.200460),
float3(-0.477903, 0.679768, 0.177961),
float3(-0.110407, -0.852001, 0.157988),
float3( 0.677789, 0.571241, 0.140256),
float3(-0.912091, 0.037718, 0.124514),
float3( 0.665301, -0.662063, 0.110540),
float3(-0.044511, 0.962596, 0.098133),
float3(-0.633036, -0.758588, 0.087119)
};

float3 blur_webcam_bg(float2 uv, float intensity, float2 qpx, float2 local_px)
{
float2 step = (max(intensity, 0.0) * 12.0 + 2.0) / max(qpx, 1.0);
float max_r_px = max(intensity, 0.0) * 22.0 + 1.5;
float2 step = max_r_px / max(qpx, 1.0);
// Interleaved Gradient Noise pour rotation aléatoire par pixel
float noise = frac(52.9829189 * frac(0.06711056 * local_px.x + 0.00583715 * local_px.y));
float angle = noise * 6.2831853;
float s, c;
sincos(angle, s, c);
float3 sum = 0.0;
float total = 0.0;
[unroll] for (int dy = -2; dy <= 2; dy++)
[unroll] for (int k = 0; k < 21; k++)
{
[unroll] for (int dx = -2; dx <= 2; dx++)
{
float w = 1.0 / (1.0 + length(float2(dx, dy)));
sum += sample_yuv(saturate(uv + float2(dx, dy) * step)) * w;
total += w;
}
float2 p = VOGEL_TAPS[k].xy;
float w = VOGEL_TAPS[k].z;
float2 rot_p = float2(p.x * c - p.y * s, p.x * s + p.y * c);
sum += sample_yuv(saturate(uv + rot_p * step)) * w;
total += w;
}
return sum / max(total, 1e-4);
}
Expand Down Expand Up @@ -468,8 +497,10 @@ float4 ps_main(VSOut i) : SV_Target
float2 localp = (i.pout - dst_prev.xy) / dst_prev.zw;
float2 uv_prev = src_prev.xy + localp * (src_prev.zw - src_prev.xy);
float2 duv = uv_now - uv_prev;
float mb_scale = saturate(mb.y);
float2 duv_blur = duv * mb_scale;
int taps = (int) mb.x;
if (taps <= 1 || dot(duv, duv) < 1e-9)
if (taps <= 1 || mb_scale <= 0.001 || dot(duv_blur, duv_blur) < 1e-9)
{
rgb = sample_yuv(uv_now);
}
Expand All @@ -480,7 +511,7 @@ float4 ps_main(VSOut i) : SV_Target
{
if (k >= taps) break;
float t = (float) k / (float) (taps - 1);
acc += sample_yuv(uv_prev + duv * t);
acc += sample_yuv(uv_now - duv_blur * (1.0 - t));
}
rgb = acc / (float) taps;
}
Expand All @@ -505,7 +536,7 @@ float4 ps_main(VSOut i) : SV_Target
}
else if (effect > 1.5)
{
rgb = lerp(blur_webcam_bg(uv_now, fx.w, quad_px), rgb, person);
rgb = lerp(blur_webcam_bg(uv_now, fx.w, quad_px, i.local), rgb, person);
}
else
{
Expand Down
Loading
Loading