diff --git a/src-tauri/src/gpu_processing.rs b/src-tauri/src/gpu_processing.rs index 530a6bbe0..85cb63685 100644 --- a/src-tauri/src/gpu_processing.rs +++ b/src-tauri/src/gpu_processing.rs @@ -382,9 +382,12 @@ impl GpuProcessor { cache: None, }); + // WGSL uniform layout may require a larger size than repr(C); round up to multiple of 16 + // so the bound buffer size satisfies the shader (see "buffer bound with size X where shader expects Y"). + let adjustments_size = std::mem::size_of::() as u64; let adjustments_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Adjustments Buffer"), - size: std::mem::size_of::() as u64, + size: ((adjustments_size + 15) / 16) * 16, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); diff --git a/src-tauri/src/image_processing.rs b/src-tauri/src/image_processing.rs index bd9019ef5..91ffab7a8 100644 --- a/src-tauri/src/image_processing.rs +++ b/src-tauri/src/image_processing.rs @@ -992,10 +992,12 @@ pub struct GlobalAdjustments { pub red_curve_count: u32, pub green_curve_count: u32, pub blue_curve_count: u32, - _pad_end1: f32, - _pad_end2: f32, - _pad_end3: f32, - _pad_end4: f32, + pub lab_curve_l: [Point; 16], + pub lab_curve_a: [Point; 16], + pub lab_curve_b: [Point; 16], + pub lab_curve_l_count: u32, + pub lab_curve_a_count: u32, + pub lab_curve_b_count: u32, pub glow_amount: f32, pub halation_amount: f32, @@ -1051,10 +1053,13 @@ pub struct MaskAdjustments { pub red_curve_count: u32, pub green_curve_count: u32, pub blue_curve_count: u32, - _pad_end4: f32, - _pad_end5: f32, - _pad_end6: f32, - _pad_end7: f32, + pub lab_curve_l: [Point; 16], + pub lab_curve_a: [Point; 16], + pub lab_curve_b: [Point; 16], + pub lab_curve_l_count: u32, + pub lab_curve_a_count: u32, + pub lab_curve_b_count: u32, + pub _pad_end: f32, } #[derive(Debug, Clone, Copy, Pod, Zeroable, Default)] @@ -1066,6 +1071,7 @@ pub struct AllAdjustments { pub tile_offset_x: u32, pub tile_offset_y: u32, pub mask_atlas_cols: u32, + pub _pad_tail: [[f32; 4]; 3], } struct AdjustmentScales { @@ -1364,6 +1370,21 @@ fn get_global_adjustments_from_json( } else { Vec::new() }; + let lab_l_points: Vec = if is_visible("curves") { + curves_obj["labL"].as_array().cloned().unwrap_or_default() + } else { + Vec::new() + }; + let lab_a_points: Vec = if is_visible("curves") { + curves_obj["labA"].as_array().cloned().unwrap_or_default() + } else { + Vec::new() + }; + let lab_b_points: Vec = if is_visible("curves") { + curves_obj["labB"].as_array().cloned().unwrap_or_default() + } else { + Vec::new() + }; let cg_obj = js_adjustments .get("colorGrading") @@ -1538,10 +1559,12 @@ fn get_global_adjustments_from_json( red_curve_count: red_points.len() as u32, green_curve_count: green_points.len() as u32, blue_curve_count: blue_points.len() as u32, - _pad_end1: 0.0, - _pad_end2: 0.0, - _pad_end3: 0.0, - _pad_end4: 0.0, + lab_curve_l: convert_points_to_aligned(lab_l_points.clone()), + lab_curve_a: convert_points_to_aligned(lab_a_points.clone()), + lab_curve_b: convert_points_to_aligned(lab_b_points.clone()), + lab_curve_l_count: lab_l_points.len() as u32, + lab_curve_a_count: lab_a_points.len() as u32, + lab_curve_b_count: lab_b_points.len() as u32, glow_amount: get_val("effects", "glowAmount", SCALES.glow, None), halation_amount: get_val("effects", "halationAmount", SCALES.halation, None), @@ -1593,6 +1616,21 @@ fn get_mask_adjustments_from_json(adj: &serde_json::Value) -> MaskAdjustments { } else { Vec::new() }; + let lab_l_points: Vec = if is_visible("curves") { + curves_obj["labL"].as_array().cloned().unwrap_or_default() + } else { + Vec::new() + }; + let lab_a_points: Vec = if is_visible("curves") { + curves_obj["labA"].as_array().cloned().unwrap_or_default() + } else { + Vec::new() + }; + let lab_b_points: Vec = if is_visible("curves") { + curves_obj["labB"].as_array().cloned().unwrap_or_default() + } else { + Vec::new() + }; let cg_obj = adj.get("colorGrading").cloned().unwrap_or_default(); MaskAdjustments { @@ -1670,10 +1708,13 @@ fn get_mask_adjustments_from_json(adj: &serde_json::Value) -> MaskAdjustments { red_curve_count: red_points.len() as u32, green_curve_count: green_points.len() as u32, blue_curve_count: blue_points.len() as u32, - _pad_end4: 0.0, - _pad_end5: 0.0, - _pad_end6: 0.0, - _pad_end7: 0.0, + lab_curve_l: convert_points_to_aligned(lab_l_points.clone()), + lab_curve_a: convert_points_to_aligned(lab_a_points.clone()), + lab_curve_b: convert_points_to_aligned(lab_b_points.clone()), + lab_curve_l_count: lab_l_points.len() as u32, + lab_curve_a_count: lab_a_points.len() as u32, + lab_curve_b_count: lab_b_points.len() as u32, + _pad_end: 0.0, } } @@ -1707,6 +1748,7 @@ pub fn get_all_adjustments_from_json( tile_offset_x: 0, tile_offset_y: 0, mask_atlas_cols: 1, + _pad_tail: [[0.0; 4]; 3], } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d03bb0643..aa56152db 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1562,6 +1562,7 @@ fn build_single_mask_adjustments(all: &AllAdjustments, mask_index: usize) -> All tile_offset_x: all.tile_offset_x, tile_offset_y: all.tile_offset_y, mask_atlas_cols: all.mask_atlas_cols, + _pad_tail: [[0.0; 4]; 3], }; single.mask_adjustments[0] = all.mask_adjustments[mask_index]; for i in 1..single.mask_adjustments.len() { diff --git a/src-tauri/src/shaders/shader.wgsl b/src-tauri/src/shaders/shader.wgsl index 1618e105a..db7fff19e 100644 --- a/src-tauri/src/shaders/shader.wgsl +++ b/src-tauri/src/shaders/shader.wgsl @@ -101,10 +101,12 @@ struct GlobalAdjustments { red_curve_count: u32, green_curve_count: u32, blue_curve_count: u32, - _pad_end1: f32, - _pad_end2: f32, - _pad_end3: f32, - _pad_end4: f32, + lab_curve_l: array, + lab_curve_a: array, + lab_curve_b: array, + lab_curve_l_count: u32, + lab_curve_a_count: u32, + lab_curve_b_count: u32, glow_amount: f32, halation_amount: f32, @@ -158,10 +160,13 @@ struct MaskAdjustments { red_curve_count: u32, green_curve_count: u32, blue_curve_count: u32, - _pad_end4: f32, - _pad_end5: f32, - _pad_end6: f32, - _pad_end7: f32, + lab_curve_l: array, + lab_curve_a: array, + lab_curve_b: array, + lab_curve_l_count: u32, + lab_curve_a_count: u32, + lab_curve_b_count: u32, + _pad_end: f32, } struct AllAdjustments { @@ -171,6 +176,7 @@ struct AllAdjustments { tile_offset_x: u32, tile_offset_y: u32, mask_atlas_cols: u32, + _pad_tail: array, 3>, } struct HslRange { @@ -236,6 +242,47 @@ fn linear_to_srgb(c: vec3) -> vec3 { return select(higher, lower, c_clamped <= cutoff); } +fn lab_f(t: f32) -> f32 { + return select(7.787 * t + 0.137931034, pow(max(t, 0.0), 0.33333334), t > 0.008856); +} + +fn lab_inv_f(t: f32) -> f32 { + return select((t - 0.137931034) / 7.787, t * t * t, t > 0.20689655); +} + +fn srgb_to_lab(c: vec3) -> vec3 { + let lin = srgb_to_linear(c); + let x = lin.r * 0.4124564 + lin.g * 0.3575761 + lin.b * 0.1804375; + let y = lin.r * 0.2126729 + lin.g * 0.7151522 + lin.b * 0.0721750; + let z = lin.r * 0.0193339 + lin.g * 0.1191920 + lin.b * 0.9503041; + let xn = 0.95047; + let yn = 1.0; + let zn = 1.08883; + let fx = lab_f(x / xn); + let fy = lab_f(y / yn); + let fz = lab_f(z / zn); + let L = 116.0 * fy - 16.0; + let a = 500.0 * (fx - fy); + let b = 200.0 * (fy - fz); + return vec3(L, a, b); +} + +fn lab_to_srgb(lab: vec3) -> vec3 { + let xn = 0.95047; + let yn = 1.0; + let zn = 1.08883; + let fy = (lab.x + 16.0) / 116.0; + let fx = lab.y / 500.0 + fy; + let fz = fy - lab.z / 200.0; + let x = xn * lab_inv_f(fx); + let y = yn * lab_inv_f(fy); + let z = zn * lab_inv_f(fz); + let r = x * 3.2404542 + y * (-1.5371385) + z * (-0.4985314); + let g = x * (-0.9692660) + y * 1.8760108 + z * 0.0415560; + let b = x * 0.0556434 + y * (-0.2040259) + z * 1.0572252; + return linear_to_srgb(vec3(r, g, b)); +} + fn rgb_to_hsv(c: vec3) -> vec3 { let c_max = max(c.r, max(c.g, c.b)); let c_min = min(c.r, min(c.g, c.b)); @@ -1025,6 +1072,24 @@ fn apply_all_curves(color: vec3, luma_curve: array, luma_curve_c } } +fn apply_lab_curves(color: vec3, lab_curve_l: array, lab_curve_l_count: u32, lab_curve_a: array, lab_curve_a_count: u32, lab_curve_b: array, lab_curve_b_count: u32) -> vec3 { + let l_def = is_default_curve(lab_curve_l, lab_curve_l_count); + let a_def = is_default_curve(lab_curve_a, lab_curve_a_count); + let b_def = is_default_curve(lab_curve_b, lab_curve_b_count); + if (l_def && a_def && b_def) { return color; } + let lab = srgb_to_lab(color); + let L_in = clamp(lab.x, 0.0, 100.0); + let a_in = clamp(lab.y, -128.0, 127.0); + let b_in = clamp(lab.z, -128.0, 127.0); + let L_norm = L_in / 100.0; + let a_norm = (a_in + 128.0) / 256.0; + let b_norm = (b_in + 128.0) / 256.0; + let L_out = clamp(apply_curve(L_norm, lab_curve_l, lab_curve_l_count) * 100.0, 0.0, 100.0); + let a_out = clamp(apply_curve(a_norm, lab_curve_a, lab_curve_a_count) * 256.0 - 128.0, -128.0, 127.0); + let b_out = clamp(apply_curve(b_norm, lab_curve_b, lab_curve_b_count) * 256.0 - 128.0, -128.0, 127.0); + return lab_to_srgb(vec3(L_out, a_out, b_out)); +} + fn apply_all_adjustments( initial_rgb: vec3, adj: GlobalAdjustments, @@ -1462,15 +1527,18 @@ fn main(@builtin(global_invocation_id) id: vec3) { adjustments.global.blue_curve, adjustments.global.blue_curve_count ); + final_rgb = apply_lab_curves(final_rgb, adjustments.global.lab_curve_l, adjustments.global.lab_curve_l_count, adjustments.global.lab_curve_a, adjustments.global.lab_curve_a_count, adjustments.global.lab_curve_b, adjustments.global.lab_curve_b_count); + for (var i = 0u; i < adjustments.mask_count; i = i + 1u) { let influence = get_mask_influence(i, absolute_coord); if (influence > 0.001) { - let mask_curved_srgb = apply_all_curves(final_rgb, + var mask_curved_srgb = apply_all_curves(final_rgb, adjustments.mask_adjustments[i].luma_curve, adjustments.mask_adjustments[i].luma_curve_count, adjustments.mask_adjustments[i].red_curve, adjustments.mask_adjustments[i].red_curve_count, adjustments.mask_adjustments[i].green_curve, adjustments.mask_adjustments[i].green_curve_count, adjustments.mask_adjustments[i].blue_curve, adjustments.mask_adjustments[i].blue_curve_count ); + mask_curved_srgb = apply_lab_curves(mask_curved_srgb, adjustments.mask_adjustments[i].lab_curve_l, adjustments.mask_adjustments[i].lab_curve_l_count, adjustments.mask_adjustments[i].lab_curve_a, adjustments.mask_adjustments[i].lab_curve_a_count, adjustments.mask_adjustments[i].lab_curve_b, adjustments.mask_adjustments[i].lab_curve_b_count); final_rgb = mix(final_rgb, mask_curved_srgb, influence); } } diff --git a/src/components/adjustments/Curves.tsx b/src/components/adjustments/Curves.tsx index 30b1eaf9f..ed273cf3a 100644 --- a/src/components/adjustments/Curves.tsx +++ b/src/components/adjustments/Curves.tsx @@ -14,6 +14,9 @@ export interface ChannelConfig { [ActiveChannel.Red]: ColorData; [ActiveChannel.Green]: ColorData; [ActiveChannel.Blue]: ColorData; + [ActiveChannel.LabL]: ColorData; + [ActiveChannel.LabA]: ColorData; + [ActiveChannel.LabB]: ColorData; } interface ColorData { @@ -284,6 +287,9 @@ export default function CurveGraph({ red: { color: '#FF6B6B', data: histogram?.red }, green: { color: '#6BCB77', data: histogram?.green }, blue: { color: '#4D96FF', data: histogram?.blue }, + labL: { color: '#E8E8E8', data: histogram?.luma }, + labA: { color: '#E86B6B', data: histogram?.red }, + labB: { color: '#4D96FF', data: histogram?.blue }, }; const propPoints = adjustments?.curves?.[activeChannel]; @@ -434,6 +440,9 @@ export default function CurveGraph({ [ActiveChannel.Red]: defaultPoints, [ActiveChannel.Green]: defaultPoints, [ActiveChannel.Blue]: defaultPoints, + [ActiveChannel.LabL]: defaultPoints, + [ActiveChannel.LabA]: defaultPoints, + [ActiveChannel.LabB]: defaultPoints, }, })); }; @@ -442,7 +451,10 @@ export default function CurveGraph({ ActiveChannel.Luma, ActiveChannel.Red, ActiveChannel.Green, - ActiveChannel.Blue + ActiveChannel.Blue, + ActiveChannel.LabL, + ActiveChannel.LabA, + ActiveChannel.LabB, ].some(channel => { if (channel === activeChannel) return false; return !isDefaultCurve(adjustments.curves?.[channel]); diff --git a/src/utils/adjustments.tsx b/src/utils/adjustments.tsx index e83a5fea4..973669c8c 100644 --- a/src/utils/adjustments.tsx +++ b/src/utils/adjustments.tsx @@ -5,6 +5,9 @@ import { SubMask, SubMaskMode } from '../components/panel/right/Masks'; export enum ActiveChannel { Blue = 'blue', Green = 'green', + LabA = 'labA', + LabB = 'labB', + LabL = 'labL', Luma = 'luma', Red = 'red', } @@ -239,6 +242,9 @@ export interface Curves { [index: string]: Array; blue: Array; green: Array; + labA: Array; + labB: Array; + labL: Array; luma: Array; red: Array; } @@ -360,6 +366,18 @@ export const INITIAL_MASK_ADJUSTMENTS: MaskAdjustments = { { x: 0, y: 0 }, { x: 255, y: 255 }, ], + labA: [ + { x: 0, y: 0 }, + { x: 255, y: 255 }, + ], + labB: [ + { x: 0, y: 0 }, + { x: 255, y: 255 }, + ], + labL: [ + { x: 0, y: 0 }, + { x: 255, y: 255 }, + ], luma: [ { x: 0, y: 0 }, { x: 255, y: 255 }, @@ -435,6 +453,18 @@ export const INITIAL_ADJUSTMENTS: Adjustments = { { x: 0, y: 0 }, { x: 255, y: 255 }, ], + labA: [ + { x: 0, y: 0 }, + { x: 255, y: 255 }, + ], + labB: [ + { x: 0, y: 0 }, + { x: 255, y: 255 }, + ], + labL: [ + { x: 0, y: 0 }, + { x: 255, y: 255 }, + ], luma: [ { x: 0, y: 0 }, { x: 255, y: 255 },