Improve smudge tool
This commit is contained in:
parent
553cc383d5
commit
bff3d660d6
|
|
@ -461,7 +461,10 @@ impl BrushEngine {
|
|||
if matches!(base_blend, RasterBlendMode::Smudge) {
|
||||
let ndx = dx / seg_len;
|
||||
let ndy = dy / seg_len;
|
||||
let smudge_dist = radius2 * bs.smudge_radius_log.exp();
|
||||
// strength=1.0 → sample from 1 dab back (drag pixels with us).
|
||||
// strength=0.0 → sample from current position (no change).
|
||||
// smudge_radius_log is repurposed as a linear [0,1] strength value here.
|
||||
let smudge_dist = spacing_px * bs.smudge_radius_log.clamp(0.0, 1.0);
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, cr, cg, cb,
|
||||
ndx, ndy, smudge_dist);
|
||||
|
|
|
|||
|
|
@ -276,15 +276,9 @@ impl GpuBrushEngine {
|
|||
|
||||
/// Dispatch the brush compute shader for `dabs` onto the canvas of `keyframe_id`.
|
||||
///
|
||||
/// All dabs for the frame are batched into a single GPU dispatch:
|
||||
/// 1. Copy the FULL canvas src→dst (so dst has all previous dabs).
|
||||
/// 2. Upload all dabs as one storage buffer.
|
||||
/// 3. Dispatch the compute shader once over the union bounding box.
|
||||
/// 4. Swap once.
|
||||
///
|
||||
/// Batching is required for correctness: a per-dab copy of only the dab's
|
||||
/// bounding box would leave all other previous dabs missing from dst after swap,
|
||||
/// causing every other dab to flicker in/out.
|
||||
/// Paint/erase dabs are batched in a single GPU dispatch with a full canvas copy.
|
||||
/// Smudge dabs are dispatched sequentially (one per dab) with a bbox-only copy
|
||||
/// so each dab reads the canvas state written by the previous dab.
|
||||
///
|
||||
/// If `dabs` is empty, does nothing.
|
||||
pub fn render_dabs(
|
||||
|
|
@ -298,12 +292,65 @@ impl GpuBrushEngine {
|
|||
canvas_h: u32,
|
||||
) {
|
||||
if dabs.is_empty() { return; }
|
||||
|
||||
// Smudge dabs must be applied one at a time so each dab reads the canvas
|
||||
// state written by the previous dab. Use bbox-only copies (union of current
|
||||
// and previous dab) to avoid an expensive full-canvas copy per dab.
|
||||
let is_smudge = dabs.first().map(|d| d.blend_mode == 2).unwrap_or(false);
|
||||
if is_smudge {
|
||||
let mut prev_bbox: Option<(i32, i32, i32, i32)> = None;
|
||||
for dab in dabs {
|
||||
let r = dab.radius + 1.0;
|
||||
let cur_bbox = (
|
||||
(dab.x - r).floor() as i32,
|
||||
(dab.y - r).floor() as i32,
|
||||
(dab.x + r).ceil() as i32,
|
||||
(dab.y + r).ceil() as i32,
|
||||
);
|
||||
// Expand copy region to include the previous dab's bbox so the
|
||||
// pixels it wrote are visible as the source for this dab's smudge.
|
||||
let copy_bbox = match prev_bbox {
|
||||
Some(pb) => (cur_bbox.0.min(pb.0), cur_bbox.1.min(pb.1),
|
||||
cur_bbox.2.max(pb.2), cur_bbox.3.max(pb.3)),
|
||||
None => cur_bbox,
|
||||
};
|
||||
self.render_dabs_batch(device, queue, keyframe_id,
|
||||
std::slice::from_ref(dab), cur_bbox, Some(copy_bbox), canvas_w, canvas_h);
|
||||
prev_bbox = Some(cur_bbox);
|
||||
}
|
||||
} else {
|
||||
self.render_dabs_batch(device, queue, keyframe_id, dabs, bbox, None, canvas_w, canvas_h);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner batch dispatch.
|
||||
///
|
||||
/// `dispatch_bbox` — region dispatched to the compute shader (usually the union of all dab bboxes).
|
||||
/// `copy_bbox` — region to copy src→dst before dispatch:
|
||||
/// - `None` → copy the full canvas (required for paint/erase batches so
|
||||
/// dabs outside the current frame's region are preserved).
|
||||
/// - `Some(r)` → copy only region `r` (sufficient for sequential smudge dabs
|
||||
/// because both textures hold identical data outside previously
|
||||
/// touched regions, so no full copy is needed).
|
||||
fn render_dabs_batch(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
keyframe_id: Uuid,
|
||||
dabs: &[GpuDab],
|
||||
dispatch_bbox: (i32, i32, i32, i32),
|
||||
copy_bbox: Option<(i32, i32, i32, i32)>,
|
||||
canvas_w: u32,
|
||||
canvas_h: u32,
|
||||
) {
|
||||
if dabs.is_empty() { return; }
|
||||
let canvas = match self.canvases.get_mut(&keyframe_id) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Clamp the union bounding box to canvas bounds.
|
||||
// Clamp the dispatch bounding box to canvas bounds.
|
||||
let bbox = dispatch_bbox;
|
||||
let x0 = bbox.0.max(0) as u32;
|
||||
let y0 = bbox.1.max(0) as u32;
|
||||
let x1 = (bbox.2 as u32).min(canvas_w);
|
||||
|
|
@ -312,11 +359,18 @@ impl GpuBrushEngine {
|
|||
let bbox_w = x1 - x0;
|
||||
let bbox_h = y1 - y0;
|
||||
|
||||
// Step 1: Copy the ENTIRE canvas src→dst so dst starts with all previous dabs.
|
||||
// A bbox-only copy would lose previous dabs outside this frame's region after swap.
|
||||
// Step 1: Copy src→dst.
|
||||
// For paint/erase batches (copy_bbox = None): copy the ENTIRE canvas so dst
|
||||
// starts with all previous dabs — a bbox-only copy would lose dabs outside
|
||||
// this frame's region after swap.
|
||||
// For smudge (copy_bbox = Some(r)): copy only the union of the current and
|
||||
// previous dab bboxes. Outside that region both textures hold identical
|
||||
// data so no full copy is needed.
|
||||
let mut copy_enc = device.create_command_encoder(
|
||||
&wgpu::CommandEncoderDescriptor { label: Some("canvas_full_copy_encoder") },
|
||||
&wgpu::CommandEncoderDescriptor { label: Some("canvas_copy_encoder") },
|
||||
);
|
||||
match copy_bbox {
|
||||
None => {
|
||||
copy_enc.copy_texture_to_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: canvas.src(),
|
||||
|
|
@ -332,6 +386,31 @@ impl GpuBrushEngine {
|
|||
},
|
||||
wgpu::Extent3d { width: canvas_w, height: canvas_h, depth_or_array_layers: 1 },
|
||||
);
|
||||
}
|
||||
Some(cb) => {
|
||||
let cx0 = cb.0.max(0) as u32;
|
||||
let cy0 = cb.1.max(0) as u32;
|
||||
let cx1 = (cb.2 as u32).min(canvas_w);
|
||||
let cy1 = (cb.3 as u32).min(canvas_h);
|
||||
if cx1 > cx0 && cy1 > cy0 {
|
||||
copy_enc.copy_texture_to_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: canvas.src(),
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d { x: cx0, y: cy0, z: 0 },
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: canvas.dst(),
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d { x: cx0, y: cy0, z: 0 },
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::Extent3d { width: cx1 - cx0, height: cy1 - cy0, depth_or_array_layers: 1 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
queue.submit(Some(copy_enc.finish()));
|
||||
|
||||
// Step 2: Upload all dabs as a single storage buffer.
|
||||
|
|
|
|||
|
|
@ -329,10 +329,9 @@ impl InfopanelPane {
|
|||
ui.add(egui::Slider::new(shared.smudge_radius, 1.0_f32..=200.0).logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Reach:");
|
||||
ui.add(egui::Slider::new(shared.smudge_strength, 0.1_f32..=5.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.2}x", v)));
|
||||
ui.label("Strength:");
|
||||
ui.add(egui::Slider::new(shared.smudge_strength, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Hardness:");
|
||||
|
|
|
|||
|
|
@ -4812,7 +4812,7 @@ impl StagePane {
|
|||
b.dabs_per_actual_radius = 0.0;
|
||||
// strength controls how far behind the stroke to sample (smudge_dist multiplier).
|
||||
// smudge_dist = radius * exp(smudge_radius_log), so log(strength) gives the ratio.
|
||||
b.smudge_radius_log = shared.smudge_strength.ln();
|
||||
b.smudge_radius_log = *shared.smudge_strength; // linear [0,1] strength
|
||||
}
|
||||
b
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue