diff --git a/src-tauri/src/image_processing.rs b/src-tauri/src/image_processing.rs index 21daca19ab..bcfbd7aedd 100644 --- a/src-tauri/src/image_processing.rs +++ b/src-tauri/src/image_processing.rs @@ -1288,13 +1288,13 @@ pub struct GlobalAdjustments { pub chromatic_aberration_blue_yellow: f32, pub show_clipping: u32, pub is_raw_image: u32, - _pad_ca1: f32, + pub skin_smoothing: f32, pub has_lut: u32, pub lut_intensity: f32, pub tonemapper_mode: u32, - _pad_lut2: f32, - _pad_lut3: f32, + pub skin_texture: f32, + pub skin_smoothing_scale: f32, _pad_lut4: f32, _pad_lut5: f32, @@ -1367,15 +1367,15 @@ pub struct MaskAdjustments { pub sharpness_threshold: f32, pub hue: f32, - _pad_cg1: f32, - _pad_cg2: f32, + pub skin_smoothing: f32, + pub skin_texture: f32, pub color_grading_shadows: ColorGradeSettings, pub color_grading_midtones: ColorGradeSettings, pub color_grading_highlights: ColorGradeSettings, pub color_grading_global: ColorGradeSettings, pub color_grading_blending: f32, pub color_grading_balance: f32, - _pad5: f32, + pub skin_smoothing_scale: f32, _pad6: f32, pub hsl: [HslColor; 8], @@ -1424,6 +1424,9 @@ struct AdjustmentScales { luma_noise_reduction: f32, color_noise_reduction: f32, clarity: f32, + skin_smoothing: f32, + skin_texture: f32, + skin_smoothing_scale: f32, dehaze: f32, structure: f32, centré: f32, @@ -1473,6 +1476,9 @@ const SCALES: AdjustmentScales = AdjustmentScales { luma_noise_reduction: 100.0, color_noise_reduction: 100.0, clarity: 200.0, + skin_smoothing: 100.0, + skin_texture: 50.0, + skin_smoothing_scale: 100.0, dehaze: 750.0, structure: 200.0, centré: 250.0, @@ -2068,15 +2074,15 @@ fn get_global_adjustments_from_json( 0 }, is_raw_image: if is_raw { 1 } else { 0 }, - _pad_ca1: 0.0, + skin_smoothing: get_val("details", "skinSmoothing", SCALES.skin_smoothing, None), has_lut, lut_intensity, tonemapper_mode: tonemapper_override .unwrap_or_else(|| if tone_mapper == "agx" { 1 } else { 0 }), - _pad_lut2: 0.0, - _pad_lut3: 0.0, + skin_texture: get_val("details", "skinTexture", SCALES.skin_texture, Some(50.0)), + skin_smoothing_scale: get_val("details", "skinSmoothingScale", SCALES.skin_smoothing_scale, None), _pad_lut4: 0.0, _pad_lut5: 0.0, @@ -2231,8 +2237,13 @@ fn get_mask_adjustments_from_json(adj: &serde_json::Value) -> MaskAdjustments { sharpness_threshold: get_val("details", "sharpnessThreshold", SCALES.sharpness_threshold), hue: get_val("color", "hue", 1.0), - _pad_cg1: 0.0, - _pad_cg2: 0.0, + skin_smoothing: get_val("details", "skinSmoothing", SCALES.skin_smoothing), + // Neutral texture is 1.0 (raw value 50); missing/hidden must not read as 0. + skin_texture: if is_visible("details") { + adj["skinTexture"].as_f64().unwrap_or(50.0) as f32 / SCALES.skin_texture + } else { + 1.0 + }, color_grading_shadows: if is_visible("color") { parse_color_grade_settings(&cg_obj["shadows"]) } else { @@ -2263,7 +2274,7 @@ fn get_mask_adjustments_from_json(adj: &serde_json::Value) -> MaskAdjustments { } else { 0.0 }, - _pad5: 0.0, + skin_smoothing_scale: get_val("details", "skinSmoothingScale", SCALES.skin_smoothing_scale), _pad6: 0.0, hsl: if is_visible("color") { diff --git a/src-tauri/src/shaders/shader.wgsl b/src-tauri/src/shaders/shader.wgsl index 7bfb5953b2..f917c62f7f 100644 --- a/src-tauri/src/shaders/shader.wgsl +++ b/src-tauri/src/shaders/shader.wgsl @@ -66,13 +66,13 @@ struct GlobalAdjustments { chromatic_aberration_blue_yellow: f32, show_clipping: u32, is_raw_image: u32, - _pad_ca1: f32, + skin_smoothing: f32, has_lut: u32, lut_intensity: f32, tonemapper_mode: u32, - _pad_lut2: f32, - _pad_lut3: f32, + skin_texture: f32, + skin_smoothing_scale: f32, _pad_lut4: f32, _pad_lut5: f32, @@ -143,15 +143,15 @@ struct MaskAdjustments { sharpness_threshold: f32, hue: f32, - _pad_cg1: f32, - _pad_cg2: f32, + skin_smoothing: f32, + skin_texture: f32, color_grading_shadows: ColorGradeSettings, color_grading_midtones: ColorGradeSettings, color_grading_highlights: ColorGradeSettings, color_grading_global: ColorGradeSettings, color_grading_blending: f32, color_grading_balance: f32, - _pad5: f32, + skin_smoothing_scale: f32, _pad6: f32, hsl: array, @@ -718,6 +718,50 @@ fn apply_color_grading(color: vec3, shadows: ColorGradeSettings, midtones: return graded_color; } +// Frequency-separation skin smoothing: flatten the blotch band while re-adding +// the fine band (pores, hair) so texture survives - unlike a plain +// negative-clarity blur mix. +fn apply_skin_smoothing( + color_linear: vec3, + clarity_blurred_input: vec3, + structure_blurred_input: vec3, + tonal_blurred_input: vec3, + amount: f32, + texture_amount: f32, + scale_amount: f32, + is_raw: u32, +) -> vec3 { + if (amount <= 0.001) { + return color_linear; + } + var clarity_blurred = clarity_blurred_input; + var structure_blurred = structure_blurred_input; + var tonal_blurred = tonal_blurred_input; + if (is_raw != 1u) { + clarity_blurred = srgb_to_linear(clarity_blurred); + structure_blurred = srgb_to_linear(structure_blurred); + tonal_blurred = srgb_to_linear(tonal_blurred); + } + // Separation point (the "radius" in classic frequency separation): blend the + // flatten base from the clarity blur (8px, micro-blotches) toward the + // structure blur (40px, large tonal patches) to match the shot's scale. + let base_blurred = mix(clarity_blurred, structure_blurred, clamp(scale_amount, 0.0, 1.0)); + // Edge guard: blotches are low-amplitude relative to their surround, while + // structural edges (eyes, brows, hair boundaries) are high-amplitude. Fade + // the smoothing out as the normalized mid-band contrast rises. + let mid_delta = color_linear - base_blurred; + let surround_luma = max(get_luma(base_blurred), 0.02); + let rel_contrast = get_luma(abs(mid_delta)) / surround_luma; + let edge_protect = 1.0 - smoothstep(0.12, 0.4, rel_contrast); + let a = clamp(amount, 0.0, 1.0) * edge_protect; + // Texture band is everything finer than the tonal blur radius (~pore scale), + // re-injected under user control. + let tf = clamp(texture_amount, 0.0, 2.0); + let fine_detail = color_linear - tonal_blurred; + let smoothed_base = mix(color_linear, base_blurred, a); + return max(smoothed_base + fine_detail * a * tf, vec3(0.0)); +} + fn apply_local_contrast( processed_color_linear: vec3, blurred_color_input_space: vec3, @@ -1482,6 +1526,9 @@ fn main(@builtin(global_invocation_id) id: vec3) { var t_clarity = adjustments.global.clarity; var t_dehaze = adjustments.global.dehaze; var t_structure = adjustments.global.structure; + var t_skin_smoothing = adjustments.global.skin_smoothing; + var t_skin_texture = adjustments.global.skin_texture - 1.0; + var t_skin_scale = adjustments.global.skin_smoothing_scale; var t_glow = adjustments.global.glow_amount; var t_halation = adjustments.global.halation_amount; var t_flare = adjustments.global.flare_amount; @@ -1520,6 +1567,9 @@ fn main(@builtin(global_invocation_id) id: vec3) { t_clarity += m.clarity * influence; t_dehaze += m.dehaze * influence; t_structure += m.structure * influence; + t_skin_smoothing += m.skin_smoothing * influence; + t_skin_texture += (m.skin_texture - 1.0) * influence; + t_skin_scale += m.skin_smoothing_scale * influence; t_glow += m.glow_amount * influence; t_halation += m.halation_amount * influence; @@ -1577,6 +1627,10 @@ fn main(@builtin(global_invocation_id) id: vec3) { } locally_contrasted_rgb += sharpness_delta; + locally_contrasted_rgb = apply_skin_smoothing( + locally_contrasted_rgb, clarity_blurred, structure_blurred, tonal_blurred, + t_skin_smoothing, 1.0 + t_skin_texture, t_skin_scale, is_raw + ); locally_contrasted_rgb = apply_local_contrast(locally_contrasted_rgb, clarity_blurred, t_clarity, is_raw, 1u, 0.0); locally_contrasted_rgb = apply_local_contrast(locally_contrasted_rgb, structure_blurred, t_structure, is_raw, 1u, 0.0); locally_contrasted_rgb = apply_centre_local_contrast(locally_contrasted_rgb, adjustments.global.centre, absolute_coord_i, clarity_blurred, is_raw); diff --git a/src/components/adjustments/Details.tsx b/src/components/adjustments/Details.tsx index 810b95a3bd..913e3734b2 100644 --- a/src/components/adjustments/Details.tsx +++ b/src/components/adjustments/Details.tsx @@ -73,6 +73,34 @@ export default function DetailsPanel({ value={adjustments.clarity} onDragStateChange={onDragStateChange} /> + handleAdjustmentChange(DetailsAdjustment.SkinSmoothing, e.target.value)} + step={1} + value={adjustments.skinSmoothing} + onDragStateChange={onDragStateChange} + /> + handleAdjustmentChange(DetailsAdjustment.SkinTexture, e.target.value)} + step={1} + value={adjustments.skinTexture} + onDragStateChange={onDragStateChange} + /> + handleAdjustmentChange(DetailsAdjustment.SkinSmoothingScale, e.target.value)} + step={1} + value={adjustments.skinSmoothingScale} + onDragStateChange={onDragStateChange} + /> = { label: 'modals.copyPaste.groups.clarityDehaze', keys: [ DetailsAdjustment.Clarity, + DetailsAdjustment.SkinSmoothing, + DetailsAdjustment.SkinTexture, + DetailsAdjustment.SkinSmoothingScale, DetailsAdjustment.Structure, DetailsAdjustment.Dehaze, DetailsAdjustment.Centré,