Onion skinning: vector-layer ghosts (tinted)
- Core compositor gains an optional screen-blend tint per CompositorLayer ([0,0,0,0] default = no-op, so existing compositing is unaffected); CompositorLayer::with_tint sets it — giving the Vello/vector path a tint hook. - For the active VECTOR layer with onion on, build ghost scenes at each neighbouring keyframe's time via render_layer_isolated (reusing the prepare's image cache), then render each scene → sRGB → linear → composite with warm/cool tint + opacity falloff, behind the current frame. Off during playback; active layer only. Completes onion skinning for all layer types (raster + vector). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c2c7aefad1
commit
10b4aa481e
|
|
@ -105,6 +105,9 @@ pub struct CompositorLayer {
|
||||||
pub opacity: f32,
|
pub opacity: f32,
|
||||||
/// Blend mode for this layer
|
/// Blend mode for this layer
|
||||||
pub blend_mode: BlendMode,
|
pub blend_mode: BlendMode,
|
||||||
|
/// Screen-blend RGB tint; `[0,0,0,0]` (the default) is a no-op. Used by
|
||||||
|
/// onion-skin ghosts (warm = past, cool = future).
|
||||||
|
pub tint: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompositorLayer {
|
impl CompositorLayer {
|
||||||
|
|
@ -113,12 +116,19 @@ impl CompositorLayer {
|
||||||
buffer,
|
buffer,
|
||||||
opacity: opacity.clamp(0.0, 1.0),
|
opacity: opacity.clamp(0.0, 1.0),
|
||||||
blend_mode,
|
blend_mode,
|
||||||
|
tint: [0.0; 4],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normal(buffer: BufferHandle, opacity: f32) -> Self {
|
pub fn normal(buffer: BufferHandle, opacity: f32) -> Self {
|
||||||
Self::new(buffer, opacity, BlendMode::Normal)
|
Self::new(buffer, opacity, BlendMode::Normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Screen-blend the layer toward an RGB tint (for onion-skin ghosts).
|
||||||
|
pub fn with_tint(mut self, r: f32, g: f32, b: f32) -> Self {
|
||||||
|
self.tint = [r, g, b, 0.0];
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uniform data for the composite shader
|
/// Uniform data for the composite shader
|
||||||
|
|
@ -129,8 +139,10 @@ pub struct CompositeUniforms {
|
||||||
pub opacity: f32,
|
pub opacity: f32,
|
||||||
/// Blend mode index
|
/// Blend mode index
|
||||||
pub blend_mode: u32,
|
pub blend_mode: u32,
|
||||||
/// Padding for alignment
|
/// Padding to 16 bytes before the vec4 tint
|
||||||
pub _padding: [u32; 2],
|
pub _padding: [u32; 2],
|
||||||
|
/// Screen-blend tint ((0,0,0) = none); `.w` unused.
|
||||||
|
pub tint: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compositor for blending layers
|
/// Compositor for blending layers
|
||||||
|
|
@ -323,6 +335,7 @@ impl Compositor {
|
||||||
opacity: layer.opacity,
|
opacity: layer.opacity,
|
||||||
blend_mode: layer.blend_mode.to_index(),
|
blend_mode: layer.blend_mode.to_index(),
|
||||||
_padding: [0, 0],
|
_padding: [0, 0],
|
||||||
|
tint: layer.tint,
|
||||||
};
|
};
|
||||||
queue.write_buffer(&uniforms_buffer, 0, bytemuck::bytes_of(&uniforms));
|
queue.write_buffer(&uniforms_buffer, 0, bytemuck::bytes_of(&uniforms));
|
||||||
|
|
||||||
|
|
@ -382,6 +395,8 @@ struct Uniforms {
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
blend_mode: u32,
|
blend_mode: u32,
|
||||||
_padding: vec2<u32>,
|
_padding: vec2<u32>,
|
||||||
|
// Screen-blend tint ((0,0,0) = no tint). Used by onion-skin ghosts.
|
||||||
|
tint: vec4<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@group(0) @binding(0) var source_tex: texture_2d<f32>;
|
@group(0) @binding(0) var source_tex: texture_2d<f32>;
|
||||||
|
|
@ -526,7 +541,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
// Apply opacity
|
// Apply opacity
|
||||||
let src_alpha = src.a * uniforms.opacity;
|
let src_alpha = src.a * uniforms.opacity;
|
||||||
|
|
||||||
|
// Screen-blend tint (recolors blacks toward the tint; no-op at tint=0).
|
||||||
|
let t = uniforms.tint.rgb;
|
||||||
|
let tinted = src.rgb + t - src.rgb * t;
|
||||||
|
|
||||||
// Output premultiplied alpha in linear color space
|
// Output premultiplied alpha in linear color space
|
||||||
return vec4<f32>(src.rgb * src_alpha, src_alpha);
|
return vec4<f32>(tinted * src_alpha, src_alpha);
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
|
||||||
|
|
@ -999,6 +999,50 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
true, // Draw checkerboard for transparent backgrounds in the UI
|
true, // Draw checkerboard for transparent backgrounds in the UI
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Onion-skin ghost scenes for the active VECTOR layer (raster ghosts are
|
||||||
|
// handled in the render loop). Built at each neighbouring keyframe's time
|
||||||
|
// via the isolated-layer renderer; rendered + tinted before the active
|
||||||
|
// scene below. `ctx.onion.enabled` is already gated off during playback.
|
||||||
|
let mut onion_vector_ghosts: Vec<(vello::Scene, [f32; 3], f32)> = Vec::new();
|
||||||
|
if self.ctx.onion.enabled && !shared.is_cpu_renderer {
|
||||||
|
if let Some(active_id) = self.ctx.active_layer_id {
|
||||||
|
if let Some(layer) = self.ctx.document.get_layer(&active_id) {
|
||||||
|
if let lightningbeam_core::layer::AnyLayer::Vector(vl) = layer {
|
||||||
|
let onion = self.ctx.onion;
|
||||||
|
let after = vl.keyframes.partition_point(|kf| kf.time <= self.ctx.playback_time);
|
||||||
|
if after > 0 {
|
||||||
|
let cur = after - 1;
|
||||||
|
let mut want: Vec<(usize, [f32; 3], f32)> = Vec::new();
|
||||||
|
for d in 1..=onion.frames_before {
|
||||||
|
if cur >= d {
|
||||||
|
want.push((cur - d, crate::panes::OnionSkinSettings::PAST_TINT,
|
||||||
|
onion.ghost_opacity(d, onion.frames_before)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for d in 1..=onion.frames_after {
|
||||||
|
if cur + d < vl.keyframes.len() {
|
||||||
|
want.push((cur + d, crate::panes::OnionSkinSettings::FUTURE_TINT,
|
||||||
|
onion.ghost_opacity(d, onion.frames_after)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (idx, tint, op) in want {
|
||||||
|
let kf_time = vl.keyframes[idx].time;
|
||||||
|
let rl = lightningbeam_core::renderer::render_layer_isolated(
|
||||||
|
&self.ctx.document, kf_time, layer, camera_transform,
|
||||||
|
&mut image_cache, &shared.video_manager,
|
||||||
|
self.ctx.webcam_frame.as_ref(),
|
||||||
|
);
|
||||||
|
if rl.has_content {
|
||||||
|
onion_vector_ghosts.push((rl.scene, tint, op));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
drop(image_cache);
|
drop(image_cache);
|
||||||
let _t_after_scene_build = std::time::Instant::now();
|
let _t_after_scene_build = std::time::Instant::now();
|
||||||
|
|
||||||
|
|
@ -1252,6 +1296,40 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
|
|
||||||
match &rendered_layer.layer_type {
|
match &rendered_layer.layer_type {
|
||||||
RenderedLayerType::Vector => {
|
RenderedLayerType::Vector => {
|
||||||
|
// Onion-skin ghosts for the active vector layer: each neighbour
|
||||||
|
// scene → sRGB → linear → composite with a screen tint + falloff,
|
||||||
|
// behind the current frame.
|
||||||
|
if Some(rendered_layer.layer_id) == self.ctx.active_layer_id {
|
||||||
|
for (ghost_scene, tint, opacity) in &onion_vector_ghosts {
|
||||||
|
let gsrgb = buffer_pool.acquire(device, layer_spec);
|
||||||
|
let ghdr = buffer_pool.acquire(device, hdr_spec);
|
||||||
|
if let (Some(gsrgb_view), Some(ghdr_view), Some(hdr_view)) = (
|
||||||
|
buffer_pool.get_view(gsrgb),
|
||||||
|
buffer_pool.get_view(ghdr),
|
||||||
|
&instance_resources.hdr_texture_view,
|
||||||
|
) {
|
||||||
|
if let Ok(mut renderer) = shared.renderer.lock() {
|
||||||
|
renderer.render_to_texture(device, queue, ghost_scene, gsrgb_view, &layer_render_params).ok();
|
||||||
|
}
|
||||||
|
let mut conv = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("onion_vec_ghost_convert"),
|
||||||
|
});
|
||||||
|
shared.srgb_to_linear.convert(device, &mut conv, gsrgb_view, ghdr_view);
|
||||||
|
queue.submit(Some(conv.finish()));
|
||||||
|
let cl = lightningbeam_core::gpu::CompositorLayer::new(
|
||||||
|
ghdr, rendered_layer.opacity * opacity, rendered_layer.blend_mode,
|
||||||
|
).with_tint(tint[0], tint[1], tint[2]);
|
||||||
|
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("onion_vec_ghost_composite"),
|
||||||
|
});
|
||||||
|
shared.compositor.composite(device, queue, &mut enc, &[cl], &buffer_pool, hdr_view, None);
|
||||||
|
queue.submit(Some(enc.finish()));
|
||||||
|
}
|
||||||
|
buffer_pool.release(gsrgb);
|
||||||
|
buffer_pool.release(ghdr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vector/group layer — render Vello scene → sRGB → linear → composite.
|
// Vector/group layer — render Vello scene → sRGB → linear → composite.
|
||||||
let srgb_handle = buffer_pool.acquire(device, layer_spec);
|
let srgb_handle = buffer_pool.acquire(device, layer_spec);
|
||||||
let hdr_layer_handle = buffer_pool.acquire(device, hdr_spec);
|
let hdr_layer_handle = buffer_pool.acquire(device, hdr_spec);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue