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
42 changes: 28 additions & 14 deletions crates/compositor/src/compositor_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2254,7 +2254,9 @@ impl Compositor {
let (wants_effect, model_path) = {
let scene = self.scene.borrow();
match scene.as_ref().and_then(|s| s.webcam_effect.as_ref()) {
Some(e) if e.shader_code() > 0.0 => (true, e.model_path.clone()),
Some(e) if e.shader_code() > 0.0 && e.shader_code() < 3.5 => {
(true, e.model_path.clone())
}
_ => (false, None),
}
};
Expand Down Expand Up @@ -2468,7 +2470,7 @@ impl Compositor {
.borrow()
.as_ref()
.and_then(|s| s.webcam_effect.as_ref())
.is_some_and(|e| e.shader_code() > 0.0);
.is_some_and(|e| e.shader_code() > 0.0 && e.shader_code() < 3.5);
if wants_seg && !webcam.is_null() {
// `nv12_srvs` dereference `data[0]` sans verifier la frame elle-meme,
// d'ou le garde de nullite au-dessus (meme condition que le draw PiP).
Expand Down Expand Up @@ -2740,15 +2742,19 @@ impl Compositor {
// lancement gracieux, le temps que l'inference rende son premier masque.
//
// Calcule ICI, avant le draw comme avant l'ombre : les deux en dependent.
let (effect_code, blur_intensity, webcam_bg) = {
let (effect_code, blur_intensity, webcam_bg, chroma_color, chroma_trail_a) = {
let has_mask = self.webcam_mask.borrow().is_some();
let effect = scene_ref
.as_ref()
.and_then(|s| s.webcam_effect.as_ref())
.filter(|_| has_mask)
.filter(|e| e.shader_code() > 3.5 || has_mask)
.map(|e| (e.shader_code(), e))
.filter(|(code, _)| *code > 0.0);
match effect {
Some((code, e)) if code > 3.5 => {
let (c, ta) = e.chroma_uniforms();
(code, 0.0, None, c, ta)
}
// Fond personnalise : on PEINT le fond dans la bulle, puis on y
// decoupe la camera par-dessus — le melange alpha donne
// `lerp(fond, camera, personne)`, soit exactement ce que la branche
Expand All @@ -2768,20 +2774,27 @@ impl Compositor {
&dummy,
)
});
(1.0, 0.0, bg)
(1.0, 0.0, bg, [0.0, 0.0, 0.0, 1.0], [0.0; 4])
}
Some((code, e)) => (code, e.blur_intensity.clamp(0.0, 1.0), None),
None => (0.0, 0.0, None),
Some((code, e)) => (
code,
e.blur_intensity.clamp(0.0, 1.0),
None,
[0.0, 0.0, 0.0, 1.0],
[0.0; 4],
),
None => (0.0, 0.0, None, [0.0, 0.0, 0.0, 1.0], [0.0; 4]),
}
};
// L'ombre se juge sur le mode DE LA SCENE, pas sur `effect_code` : le fond
// personnalise se compose desormais en detourage (code 1) tout en gardant
// sa bulle, et tester le code compose la lui retirerait. Meme lecture que
// `is_cutout` cote Windows.
let is_cutout = matches!(
scene_ref.as_ref().and_then(|s| s.webcam_effect.as_ref()),
Some(e) if e.shader_code() == 1.0
) && self.webcam_mask.borrow().is_some();
let is_cutout = match scene_ref.as_ref().and_then(|s| s.webcam_effect.as_ref()) {
Some(e) if e.shader_code() > 3.5 => true,
Some(e) if e.shader_code() == 1.0 => self.webcam_mask.borrow().is_some(),
_ => false,
};
let webcam_draw = webcam_planes.as_ref().map(|(wy, wu, wv)| {
// COVER-CROP. `src` etait cable a [0,0,1,1], donc la texture entiere
// etait etiree sur la boite quelle que soit sa forme : le facteur de
Expand Down Expand Up @@ -2818,9 +2831,9 @@ impl Compositor {
quad_px: g.w_px,
radius_px: g.w_radius,
mode: 0.0,
// `color.a` porte l'alpha du decoupage (`color.a * personne`) ; le
// RGB n'est plus lu, le fond ayant deja ete peint sous la camera.
color: [0.0, 0.0, 0.0, 1.0],
// `color.a` porte l'alpha du decoupage (`color.a * personne`) ; en mode
// chromakey, `color.rgb` porte la couleur cle et `trail_a` la tolerance.
color: chroma_color,
// `fx.xy` = etendue valide de la texture webcam, par quoi le
// shader divise `uv` pour retomber dans l'espace du masque ;
// `fx.z` = mode, `fx.w` = intensite du flou. Contrat commun aux
Expand All @@ -2829,6 +2842,7 @@ impl Compositor {
src_prev: [u0, cv0, u1, cv1],
dst_prev: g.w_dst_prev,
mb: [g.mb_taps, g.mb_amount, 1.0, 0.0],
trail_a: chroma_trail_a,
..Default::default()
};
// Le masque est lie par `make_bind` sur tous les draws, pas seulement
Expand Down
32 changes: 22 additions & 10 deletions crates/compositor/src/compositor_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2193,7 +2193,9 @@ impl Compositor {
let (wants_effect, model_path) = {
let scene = self.scene.borrow();
match scene.as_ref().and_then(|s| s.webcam_effect.as_ref()) {
Some(e) if e.shader_code() > 0.0 => (true, e.model_path.clone()),
Some(e) if e.shader_code() > 0.0 && e.shader_code() < 3.5 => {
(true, e.model_path.clone())
}
_ => (false, None),
}
};
Expand Down Expand Up @@ -2932,7 +2934,7 @@ impl Compositor {
let effect = scene_ref
.as_ref()
.and_then(|s| s.webcam_effect.as_ref())
.filter(|_| mask.is_some())
.filter(|e| e.shader_code() > 3.5 || mask.is_some())
.map(|e| (e.shader_code(), e))
.filter(|(code, _)| *code > 0.0);

Expand All @@ -2941,7 +2943,7 @@ impl Compositor {
// sur le code de la SCÈNE et non sur celui envoyé au shader : le fond personnalisé
// part lui aussi en détourage ci-dessous, mais sa bulle, elle, est bien peinte et
// garde donc son ombre.
let is_cutout = matches!(effect, Some((code, _)) if code == 1.0);
let is_cutout = matches!(effect, Some((code, _)) if code == 1.0 || code > 3.5);
if cfg.shadow && !webcam_is_block && !is_cutout && g.shape_fade > 0.0 {
self.draw_shadow(
enc,
Expand All @@ -2960,13 +2962,22 @@ impl Compositor {
// fond. Le shader ne sait peindre qu'une couleur plate sous le masque ; dégradés et
// images y tombaient sur du noir, et le défaut EST une image. L'ordre est imposé :
// ombre, puis fond, puis caméra.
let (effect_code, blur_intensity) = match effect {
let (effect_code, blur_intensity, color, trail_a) = match effect {
Some((code, e)) if code > 3.5 => {
let (c, ta) = e.chroma_uniforms();
(code, 0.0, c, ta)
}
Some((code, e)) if code > 2.5 => {
self.draw_webcam_bg(enc, e.background.as_ref(), g.w_dst, g.w_px, g.w_radius);
(1.0, 0.0)
(1.0, 0.0, [0.0, 0.0, 0.0, 1.0], [0.0; 4])
}
Some((code, e)) => (code, e.blur_intensity.clamp(0.0, 1.0)),
None => (0.0, 0.0),
Some((code, e)) => (
code,
e.blur_intensity.clamp(0.0, 1.0),
[0.0, 0.0, 0.0, 1.0],
[0.0; 4],
),
None => (0.0, 0.0, [0.0, 0.0, 0.0, 1.0], [0.0; 4]),
};

// Metal tolère l'index 3 non lié tant que `fx.z` reste à 0 : la branche n'est
Expand All @@ -2990,13 +3001,14 @@ impl Compositor {
quad_px: g.w_px,
radius_px: g.w_radius,
mode: 0.0,
// `color.a` porte l'alpha du découpage (`color.a * personne`) ; le RGB n'est
// plus lu, le fond ayant déjà été peint sous la caméra.
color: [0.0, 0.0, 0.0, 1.0],
// `color.a` porte l'alpha du découpage (`color.a * personne`) ; en mode
// chromakey, `color.rgb` porte la couleur clé et `trail_a` la tolérance.
color,
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, g.mb_amount, 1.0, 0.0],
trail_a,
..Default::default()
},
wy,
Expand Down
39 changes: 26 additions & 13 deletions crates/compositor/src/compositor_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1860,7 +1860,9 @@ impl Compositor {
let (wants_effect, model_path) = {
let scene = self.scene.borrow();
match scene.as_ref().and_then(|s| s.webcam_effect.as_ref()) {
Some(e) if e.shader_code() > 0.0 => (true, e.model_path.clone()),
Some(e) if e.shader_code() > 0.0 && e.shader_code() < 3.5 => {
(true, e.model_path.clone())
}
_ => (false, None),
}
};
Expand Down Expand Up @@ -2736,10 +2738,11 @@ impl Compositor {
);
// L'ombre appartient à la bulle PiP. En détourage il n'y a plus de bulle — une
// ombre portée par un rectangle invisible se lit comme un artefact.
let is_cutout = matches!(
scene_ref.as_ref().and_then(|s| s.webcam_effect.as_ref()),
Some(e) if e.shader_code() == 1.0
) && self.webcam_mask.borrow().is_some();
let is_cutout = match scene_ref.as_ref().and_then(|s| s.webcam_effect.as_ref()) {
Some(e) if e.shader_code() > 3.5 => true,
Some(e) if e.shader_code() == 1.0 => self.webcam_mask.borrow().is_some(),
_ => false,
};
if cfg.shadow && !webcam_is_block && !is_cutout && shape_fade > 0.0 {
let strength = WEBCAM_SHADOW_OPACITY * shape_fade;
self.draw_shadow(
Expand All @@ -2759,7 +2762,7 @@ impl Compositor {
let effect = scene_ref
.as_ref()
.and_then(|s| s.webcam_effect.as_ref())
.filter(|_| mask.is_some())
.filter(|e| e.shader_code() > 3.5 || mask.is_some())
.map(|e| (e.shader_code(), e))
.filter(|(code, _)| *code > 0.0);

Expand All @@ -2768,13 +2771,22 @@ impl Compositor {
// ce que la branche « mode 3 » du shader calculait, mais pour les TROIS sortes de
// fond. Le shader ne sait peindre qu'une couleur plate sous le masque ; dégradés et
// images y tombaient sur du noir, et le défaut EST une image.
let (effect_code, blur_intensity) = match effect {
let (effect_code, blur_intensity, color, trail_a) = match effect {
Some((code, e)) if code > 3.5 => {
let (c, ta) = e.chroma_uniforms();
(code, 0.0, c, ta)
}
Some((code, e)) if code > 2.5 => {
self.draw_webcam_bg(e.background.as_ref(), w_dst, w_px, w_radius);
(1.0, 0.0)
(1.0, 0.0, [0.0, 0.0, 0.0, 1.0], [0.0; 4])
}
Some((code, e)) => (code, e.blur_intensity.clamp(0.0, 1.0)),
None => (0.0, 0.0),
Some((code, e)) => (
code,
e.blur_intensity.clamp(0.0, 1.0),
[0.0, 0.0, 0.0, 1.0],
[0.0; 4],
),
None => (0.0, 0.0, [0.0, 0.0, 0.0, 1.0], [0.0; 4]),
};

if let Some(m) = mask.as_ref() {
Expand All @@ -2790,13 +2802,14 @@ impl Compositor {
quad_px: w_px,
radius_px: w_radius,
mode: 0.0,
// `color.a` porte l'alpha du découpage (`color.a * personne`) ; le RGB n'est
// plus lu, le fond ayant déjà été peint sous la caméra.
color: [0.0, 0.0, 0.0, 1.0],
// `color.a` porte l'alpha du découpage (`color.a * personne`) ; en mode
// chromakey, `color.rgb` porte la couleur clé et `trail_a` la tolérance.
color,
fx: [w_valid[0], w_valid[1], effect_code, blur_intensity],
src_prev: [u0, sv0, u1, sv1], // src fixe (pas de zoom webcam)
dst_prev: w_dst_prev,
mb: [mb_taps, mb_amount, 1.0, 0.0],
trail_a,
..Default::default()
},
&wy,
Expand Down
59 changes: 57 additions & 2 deletions crates/compositor/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -798,10 +798,36 @@ pub struct SceneOutput {
/// la segmentation qui tourne dans ce processus (`segmentation.rs`) et arrive au shader comme
/// texture `t3`. Une version antérieure faisait cuire le composite côté app et l'envoyait
/// comme piste vidéo : le codec ne sait pas porter l'alpha, et preview et export divergeaient.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SceneChromaKey {
#[serde(default = "default_chroma_color")]
pub color: String,
#[serde(default = "default_chroma_similarity")]
pub similarity: f32,
#[serde(default = "default_chroma_smoothness")]
pub smoothness: f32,
#[serde(default = "default_chroma_spill")]
pub spill: f32,
}

fn default_chroma_color() -> String {
"#00FF00".to_string()
}
fn default_chroma_similarity() -> f32 {
0.4
}
fn default_chroma_smoothness() -> f32 {
0.1
}
fn default_chroma_spill() -> f32 {
0.5
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SceneWebcamEffect {
/// "none" | "transparent" | "blur" | "custom"
/// "none" | "transparent" | "blur" | "custom" | "chromakey"
pub mode: String,
/// 0..1, seulement pour `blur`.
#[serde(default)]
Expand All @@ -814,19 +840,45 @@ pub struct SceneWebcamEffect {
/// natif ne devine pas. Absent = pas de segmentation, l'effet reste éteint.
#[serde(default)]
pub model_path: Option<String>,
/// Paramètres chroma key (fond vert) quand `mode == "chromakey"`.
#[serde(default)]
pub chroma_key: Option<SceneChromaKey>,
}

impl SceneWebcamEffect {
/// Code passé au shader dans `fx.z` : 0 = aucun (la webcam se dessine telle quelle),
/// 1 = détourage, 2 = flou, 3 = fond personnalisé.
/// 1 = détourage, 2 = flou, 3 = fond personnalisé, 4 = chroma key (fond vert).
pub(crate) fn shader_code(&self) -> f32 {
match self.mode.as_str() {
"transparent" => 1.0,
"blur" => 2.0,
"custom" => 3.0,
"chromakey" => 4.0,
_ => 0.0,
}
}

/// Renvoie `(color, trail_a)` pour le `LayerCB` de la webcam.
/// En mode `"chromakey"`, `color` porte `[key_r, key_g, key_b, 1.0]` et
/// `trail_a` porte `[similarity, smoothness, spill, 0.0]`.
pub(crate) fn chroma_uniforms(&self) -> ([f32; 4], [f32; 4]) {
if self.mode == "chromakey" {
if let Some(ck) = &self.chroma_key {
let mut c = crate::frame_geometry::parse_hex(&ck.color)
.unwrap_or([0.0, 1.0, 0.0, 1.0]);
c[3] = 1.0;
let params = [
ck.similarity.clamp(0.01, 0.95),
ck.smoothness.clamp(0.005, 0.5),
ck.spill.clamp(0.0, 1.0),
0.0,
];
return (c, params);
}
return ([0.0, 1.0, 0.0, 1.0], [0.4, 0.1, 0.5, 0.0]);
}
([0.0, 0.0, 0.0, 1.0], [0.0; 4])
}
}

/// Tout ce dont le natif a besoin pour composer la scène, sérialisé depuis un document.
Expand Down Expand Up @@ -1146,6 +1198,9 @@ mod tests {
assert_eq!(scene_with(r#"{"mode":"transparent"}"#).shader_code(), 1.0);
assert_eq!(scene_with(r#"{"mode":"blur","blurIntensity":0.75}"#).shader_code(), 2.0);
assert_eq!(scene_with(r#"{"mode":"custom"}"#).shader_code(), 3.0);
assert_eq!(scene_with(r#"{"mode":"chromakey"}"#).shader_code(), 4.0);
let ck = scene_with(r##"{"mode":"chromakey","chromaKey":{"color":"#00FF00","similarity":0.35,"smoothness":0.15,"spill":0.6}}"##);
assert_eq!(ck.chroma_uniforms(), ([0.0, 1.0, 0.0, 1.0], [0.35, 0.15, 0.6, 0.0]));
// Un mode inconnu (document trafiqué, schéma futur) ne doit pas allumer un effet.
assert_eq!(scene_with(r#"{"mode":"hologram"}"#).shader_code(), 0.0);

Expand Down
34 changes: 33 additions & 1 deletion crates/compositor/src/shaders.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -4269,7 +4269,39 @@ float4 ps_main(VSOut i) : SV_Target
// premiere frame : `person` vaut alors 0 et le mode 1 rendrait la webcam invisible, donc
// c'est l'appelant qui ne met fx.z a autre chose que 0 qu'une fois un masque disponible.
float effect = fx.z;
if (effect > 0.5)
if (effect > 3.5)
{
float3 key_rgb = color.rgb;
float2 cur_cbcr = float2(
-0.168736 * rgb.r - 0.331264 * rgb.g + 0.5 * rgb.b,
0.5 * rgb.r - 0.418688 * rgb.g - 0.081312 * rgb.b
);
float2 key_cbcr = float2(
-0.168736 * key_rgb.r - 0.331264 * key_rgb.g + 0.5 * key_rgb.b,
0.5 * key_rgb.r - 0.418688 * key_rgb.g - 0.081312 * key_rgb.b
);
float dist = length(cur_cbcr - key_cbcr);
float similarity = max(trail_a.x, 0.001);
float smoothness = max(trail_a.y, 0.001);
alpha_mask = smoothstep(similarity, similarity + smoothness, dist);
float spill = saturate(trail_a.z);
if (spill > 0.0)
{
if (key_rgb.g >= key_rgb.r && key_rgb.g >= key_rgb.b)
{
rgb.g = lerp(rgb.g, min(rgb.g, max(rgb.r, rgb.b)), spill);
}
else if (key_rgb.b >= key_rgb.r && key_rgb.b >= key_rgb.g)
{
rgb.b = lerp(rgb.b, min(rgb.b, max(rgb.r, rgb.g)), spill);
}
else
{
rgb.r = lerp(rgb.r, min(rgb.r, max(rgb.g, rgb.b)), spill);
}
}
}
else if (effect > 0.5)
{
float2 mask_uv = uv_now / max(fx.xy, 1e-6);
float person = saturate(texMask.Sample(samp, mask_uv));
Expand Down
Loading
Loading