wgpu: Implement Bevel filter

This commit is contained in:
Nathan Adams 2023-07-13 00:52:56 +02:00
parent 771f568509
commit b1ba144166
25 changed files with 510 additions and 1 deletions

View File

@ -0,0 +1,84 @@
struct Filter {
highlight_color: vec4<f32>,
shadow_color: vec4<f32>,
strength: f32,
bevel_type: u32,
knockout: u32,
composite_source: u32,
}
@group(0) @binding(0) var texture: texture_2d<f32>;
@group(0) @binding(1) var texture_sampler: sampler;
@group(0) @binding(2) var<uniform> filter_args: Filter;
@group(0) @binding(3) var blurred: texture_2d<f32>;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) source_uv: vec2<f32>,
@location(1) blur_uv_left: vec2<f32>,
@location(2) blur_uv_right: vec2<f32>,
};
struct VertexInput {
/// The position of the vertex in texture space (topleft 0,0, bottomright 1,1)
@location(0) position: vec2<f32>,
/// The coordinate of the source texture to sample in texture space (topleft 0,0, bottomright 1,1)
@location(1) source_uv: vec2<f32>,
/// The coordinate of the blur texture to sample in texture space (topleft 0,0, bottomright 1,1)
@location(2) blur_uv_left: vec2<f32>,
/// The coordinate of the blur texture to sample in texture space (topleft 0,0, bottomright 1,1)
@location(3) blur_uv_right: vec2<f32>,
};
@vertex
fn main_vertex(in: VertexInput) -> VertexOutput {
// Convert texture space (topleft 0,0 to bottomright 1,1) to render space (topleft -1,1 to bottomright 1,-1)
let pos = vec4<f32>((in.position.x * 2.0 - 1.0), (1.0 - in.position.y * 2.0), 0.0, 1.0);
return VertexOutput(pos, in.source_uv, in.blur_uv_left, in.blur_uv_right);
}
@fragment
fn main_fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let knockout = filter_args.knockout > 0u;
let composite_source = filter_args.composite_source > 0u;
var blur_left = textureSample(blurred, texture_sampler, in.blur_uv_left).a;
var blur_right = textureSample(blurred, texture_sampler, in.blur_uv_right).a;
var dest = textureSample(texture, texture_sampler, in.source_uv);
let outer = filter_args.bevel_type == 0u || filter_args.bevel_type == 2u;
let inner = filter_args.bevel_type == 1u || filter_args.bevel_type == 2u;
if (in.blur_uv_left.x < 0.0 || in.blur_uv_left.x > 1.0 || in.blur_uv_left.y < 0.0 || in.blur_uv_left.y > 1.0) {
blur_left = 0.0;
}
if (in.blur_uv_right.x < 0.0 || in.blur_uv_right.x > 1.0 || in.blur_uv_right.y < 0.0 || in.blur_uv_right.y > 1.0) {
blur_right = 0.0;
}
let highlight_alpha = saturate((blur_left - blur_right) * filter_args.strength);
let shadow_alpha = saturate((blur_right - blur_left) * filter_args.strength);
let glow = filter_args.highlight_color * highlight_alpha + filter_args.shadow_color * shadow_alpha;
if (inner && outer) {
if (knockout) {
return glow;
} else {
return dest - dest * glow.a + glow;
}
} else if (inner) {
if (knockout) {
return glow * dest.a;
} else {
return glow * dest.a + dest * (1.0 - glow.a);
}
} else {
if (knockout) {
return glow - glow * dest.a;
} else {
return dest + glow - glow * dest.a;
}
}
}

View File

@ -810,6 +810,7 @@ impl<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
| Filter::DropShadowFilter(_) | Filter::DropShadowFilter(_)
| Filter::ColorMatrixFilter(_) | Filter::ColorMatrixFilter(_)
| Filter::ShaderFilter(_) | Filter::ShaderFilter(_)
| Filter::BevelFilter(_)
) )
} }

View File

@ -1,3 +1,4 @@
mod bevel;
mod blur; mod blur;
mod color_matrix; mod color_matrix;
mod drop_shadow; mod drop_shadow;
@ -9,6 +10,7 @@ use std::sync::{Mutex, OnceLock};
use crate::buffer_pool::TexturePool; use crate::buffer_pool::TexturePool;
use crate::descriptors::Descriptors; use crate::descriptors::Descriptors;
use crate::filters::bevel::BevelFilter;
use crate::filters::blur::BlurFilter; use crate::filters::blur::BlurFilter;
use crate::filters::color_matrix::ColorMatrixFilter; use crate::filters::color_matrix::ColorMatrixFilter;
use crate::filters::drop_shadow::DropShadowFilter; use crate::filters::drop_shadow::DropShadowFilter;
@ -130,6 +132,73 @@ impl<'a> FilterSource<'a> {
usage: wgpu::BufferUsages::VERTEX, usage: wgpu::BufferUsages::VERTEX,
}) })
} }
pub fn vertices_with_highlight_and_shadow(
&self,
device: &wgpu::Device,
blur_offset: (f32, f32),
) -> wgpu::Buffer {
let source_width = self.texture.width() as f32;
let source_height = self.texture.height() as f32;
let source_left = self.point.0 as f32;
let source_top = self.point.1 as f32;
let source_right = (self.point.0 + self.size.0) as f32;
let source_bottom = (self.point.1 + self.size.1) as f32;
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: create_debug_label!("Filter vertices").as_deref(),
contents: bytemuck::cast_slice(&[
FilterVertexWithDoubleBlur {
position: [0.0, 0.0],
source_uv: [source_left / source_width, source_top / source_height],
blur_uv_left: [
(source_left + blur_offset.0) / source_width,
(source_top + blur_offset.1) / source_height,
],
blur_uv_right: [
(source_left - blur_offset.0) / source_width,
(source_top - blur_offset.1) / source_height,
],
},
FilterVertexWithDoubleBlur {
position: [1.0, 0.0],
source_uv: [source_right / source_width, source_top / source_height],
blur_uv_left: [
(source_right + blur_offset.0) / source_width,
(source_top + blur_offset.1) / source_height,
],
blur_uv_right: [
(source_right - blur_offset.0) / source_width,
(source_top - blur_offset.1) / source_height,
],
},
FilterVertexWithDoubleBlur {
position: [1.0, 1.0],
source_uv: [source_right / source_width, source_bottom / source_height],
blur_uv_left: [
(source_right + blur_offset.0) / source_width,
(source_bottom + blur_offset.1) / source_height,
],
blur_uv_right: [
(source_right - blur_offset.0) / source_width,
(source_bottom - blur_offset.1) / source_height,
],
},
FilterVertexWithDoubleBlur {
position: [0.0, 1.0],
source_uv: [source_left / source_width, source_bottom / source_height],
blur_uv_left: [
(source_left + blur_offset.0) / source_width,
(source_bottom + blur_offset.1) / source_height,
],
blur_uv_right: [
(source_left - blur_offset.0) / source_width,
(source_bottom - blur_offset.1) / source_height,
],
},
]),
usage: wgpu::BufferUsages::VERTEX,
})
}
} }
pub struct Filters { pub struct Filters {
@ -137,6 +206,7 @@ pub struct Filters {
pub color_matrix: ColorMatrixFilter, pub color_matrix: ColorMatrixFilter,
pub shader: ShaderFilter, pub shader: ShaderFilter,
pub glow: GlowFilter, pub glow: GlowFilter,
pub bevel: BevelFilter,
} }
impl Filters { impl Filters {
@ -146,6 +216,7 @@ impl Filters {
color_matrix: ColorMatrixFilter::new(device), color_matrix: ColorMatrixFilter::new(device),
shader: ShaderFilter::new(), shader: ShaderFilter::new(),
glow: GlowFilter::new(device), glow: GlowFilter::new(device),
bevel: BevelFilter::new(device),
} }
} }
@ -163,6 +234,10 @@ impl Filters {
Filter::DropShadowFilter(filter) => { Filter::DropShadowFilter(filter) => {
DropShadowFilter::calculate_dest_rect(filter, source_rect, &self.blur, &self.glow) DropShadowFilter::calculate_dest_rect(filter, source_rect, &self.blur, &self.glow)
} }
Filter::BevelFilter(filter) => {
self.bevel
.calculate_dest_rect(filter, source_rect, &self.blur)
}
_ => source_rect, _ => source_rect,
} }
} }
@ -215,10 +290,17 @@ impl Filters {
&self.blur, &self.blur,
&self.glow, &self.glow,
)), )),
Filter::BevelFilter(filter) => Some(descriptors.filters.bevel.apply(
descriptors,
texture_pool,
draw_encoder,
&source,
&filter,
&self.blur,
)),
filter => { filter => {
static WARNED_FILTERS: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new(); static WARNED_FILTERS: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
let name = match filter { let name = match filter {
Filter::BevelFilter(_) => "BevelFilter",
Filter::GradientGlowFilter(_) => "GradientGlowFilter", Filter::GradientGlowFilter(_) => "GradientGlowFilter",
Filter::GradientBevelFilter(_) => "GradientBevelFilter", Filter::GradientBevelFilter(_) => "GradientBevelFilter",
Filter::ConvolutionFilter(_) => "ConvolutionFilter", Filter::ConvolutionFilter(_) => "ConvolutionFilter",
@ -227,6 +309,7 @@ impl Filters {
| Filter::BlurFilter(_) | Filter::BlurFilter(_)
| Filter::GlowFilter(_) | Filter::GlowFilter(_)
| Filter::DropShadowFilter(_) | Filter::DropShadowFilter(_)
| Filter::BevelFilter(_)
| Filter::ShaderFilter(_) => unreachable!(), | Filter::ShaderFilter(_) => unreachable!(),
}; };
// Only warn once per filter type // Only warn once per filter type
@ -297,3 +380,24 @@ pub const VERTEX_BUFFERS_DESCRIPTION_FILTERS_WITH_BLUR: [wgpu::VertexBufferLayou
2 => Float32x2, 2 => Float32x2,
], ],
}]; }];
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct FilterVertexWithDoubleBlur {
pub position: [f32; 2],
pub source_uv: [f32; 2],
pub blur_uv_left: [f32; 2],
pub blur_uv_right: [f32; 2],
}
pub const VERTEX_BUFFERS_DESCRIPTION_FILTERS_WITH_DOUBLE_BLUR: [wgpu::VertexBufferLayout; 1] =
[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<FilterVertexWithDoubleBlur>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x2,
3 => Float32x2,
],
}];

View File

@ -0,0 +1,283 @@
use crate::backend::RenderTargetMode;
use crate::buffer_pool::TexturePool;
use crate::descriptors::Descriptors;
use crate::filters::blur::BlurFilter;
use crate::filters::{FilterSource, VERTEX_BUFFERS_DESCRIPTION_FILTERS_WITH_DOUBLE_BLUR};
use crate::surface::target::CommandTarget;
use crate::utils::SampleCountMap;
use bytemuck::{Pod, Zeroable};
use std::sync::OnceLock;
use swf::{BevelFilter as BevelFilterArgs, Rectangle};
use wgpu::util::DeviceExt;
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable, PartialEq)]
struct BevelUniform {
highlight_color: [f32; 4],
shadow_color: [f32; 4],
strength: f32,
bevel_type: u32, // 0 outer, 1 inner, 2 full
knockout: u32, // a wasteful bool, but we need to be aligned anyway
composite_source: u32, // undocumented flash feature, another bool
}
pub struct BevelFilter {
bind_group_layout: wgpu::BindGroupLayout,
pipeline_layout: wgpu::PipelineLayout,
pipeline: SampleCountMap<OnceLock<wgpu::RenderPipeline>>,
}
impl BevelFilter {
pub fn new(device: &wgpu::Device) -> Self {
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<BevelUniform>() as u64,
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
},
count: None,
},
],
label: create_debug_label!("Bevel filter binds").as_deref(),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
Self {
pipeline: Default::default(),
pipeline_layout,
bind_group_layout,
}
}
fn pipeline(&self, descriptors: &Descriptors, msaa_sample_count: u32) -> &wgpu::RenderPipeline {
self.pipeline.get_or_init(msaa_sample_count, || {
let label = create_debug_label!("Bevel Filter ({} msaa)", msaa_sample_count);
descriptors
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: label.as_deref(),
layout: Some(&self.pipeline_layout),
vertex: wgpu::VertexState {
module: &descriptors.shaders.bevel_filter,
entry_point: "main_vertex",
buffers: &VERTEX_BUFFERS_DESCRIPTION_FILTERS_WITH_DOUBLE_BLUR,
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::default(),
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: msaa_sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &descriptors.shaders.bevel_filter,
entry_point: "main_fragment",
targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())],
}),
multiview: None,
})
})
}
pub fn calculate_dest_rect(
&self,
filter: &BevelFilterArgs,
source_rect: Rectangle<i32>,
blur_filter: &BlurFilter,
) -> Rectangle<i32> {
let mut result = blur_filter.calculate_dest_rect(&filter.inner_blur_filter(), source_rect);
let distance = filter.distance.to_f32();
let angle = filter.angle.to_f32();
let x = (angle.cos() * distance).ceil() as i32;
let y = (angle.sin() * distance).ceil() as i32;
if x < 0 {
result.x_min += x;
result.x_max -= x;
} else {
result.x_max += x;
result.x_min -= x;
}
if y < 0 {
result.y_min += y;
result.y_max -= y;
} else {
result.y_max += y;
result.y_min -= y;
}
result
}
#[allow(clippy::too_many_arguments)]
pub fn apply(
&self,
descriptors: &Descriptors,
texture_pool: &mut TexturePool,
draw_encoder: &mut wgpu::CommandEncoder,
source: &FilterSource,
filter: &BevelFilterArgs,
blur_filter: &BlurFilter,
) -> CommandTarget {
let sample_count = source.texture.sample_count();
let format = source.texture.format();
let pipeline = self.pipeline(descriptors, sample_count);
let blurred = blur_filter.apply(
descriptors,
texture_pool,
draw_encoder,
source,
&filter.inner_blur_filter(),
);
let blurred_texture = if let Some(blurred) = &blurred {
blurred.ensure_cleared(draw_encoder);
blurred.color_texture()
} else {
source.texture
};
let source_view = source.texture.create_view(&Default::default());
let blurred_view = blurred_texture.create_view(&Default::default());
let distance = filter.distance.to_f32();
let angle = filter.angle.to_f32();
let blur_offset = (angle.cos() * distance, angle.sin() * distance);
let target = CommandTarget::new(
descriptors,
texture_pool,
wgpu::Extent3d {
width: source.size.0,
height: source.size.1,
depth_or_array_layers: 1,
},
format,
sample_count,
RenderTargetMode::FreshWithColor(wgpu::Color::TRANSPARENT),
draw_encoder,
);
let mut highlight_color = [
f32::from(filter.highlight_color.r) / 255.0,
f32::from(filter.highlight_color.g) / 255.0,
f32::from(filter.highlight_color.b) / 255.0,
f32::from(filter.highlight_color.a) / 255.0,
];
highlight_color[0] *= highlight_color[3];
highlight_color[1] *= highlight_color[3];
highlight_color[2] *= highlight_color[3];
let mut shadow_color = [
f32::from(filter.shadow_color.r) / 255.0,
f32::from(filter.shadow_color.g) / 255.0,
f32::from(filter.shadow_color.b) / 255.0,
f32::from(filter.shadow_color.a) / 255.0,
];
shadow_color[0] *= shadow_color[3];
shadow_color[1] *= shadow_color[3];
shadow_color[2] *= shadow_color[3];
let buffer = descriptors
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: create_debug_label!("Filter arguments").as_deref(),
contents: bytemuck::cast_slice(&[BevelUniform {
highlight_color,
shadow_color,
strength: filter.strength.to_f32(),
bevel_type: if filter.is_on_top() {
2
} else if filter.is_inner() {
1
} else {
0
},
knockout: if filter.is_knockout() { 1 } else { 0 },
composite_source: 1,
}]),
usage: wgpu::BufferUsages::UNIFORM,
});
let vertices = source.vertices_with_highlight_and_shadow(&descriptors.device, blur_offset);
let filter_group = descriptors
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: create_debug_label!("Filter group").as_deref(),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&source_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(
descriptors.bitmap_samplers.get_sampler(false, false),
),
},
wgpu::BindGroupEntry {
binding: 2,
resource: buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&blurred_view),
},
],
});
let mut render_pass = draw_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: create_debug_label!("Bevel filter").as_deref(),
color_attachments: &[target.color_attachments()],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, &filter_group, &[]);
render_pass.set_vertex_buffer(0, vertices.slice(..));
render_pass.set_index_buffer(
descriptors.quad.indices.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..6, 0, 0..1);
drop(render_pass);
target
}
}

View File

@ -24,6 +24,7 @@ pub struct Shaders {
pub color_matrix_filter: wgpu::ShaderModule, pub color_matrix_filter: wgpu::ShaderModule,
pub blur_filter: wgpu::ShaderModule, pub blur_filter: wgpu::ShaderModule,
pub glow_filter: wgpu::ShaderModule, pub glow_filter: wgpu::ShaderModule,
pub bevel_filter: wgpu::ShaderModule,
} }
impl Shaders { impl Shaders {
@ -95,6 +96,13 @@ impl Shaders {
"filter/glow.wgsl", "filter/glow.wgsl",
include_str!("../shaders/filter/glow.wgsl"), include_str!("../shaders/filter/glow.wgsl"),
); );
let bevel_filter = make_shader(
device,
&mut composer,
&shader_defs,
"filter/bevel.wgsl",
include_str!("../shaders/filter/bevel.wgsl"),
);
let gradient_shader = make_shader( let gradient_shader = make_shader(
device, device,
&mut composer, &mut composer,
@ -126,6 +134,7 @@ impl Shaders {
color_matrix_filter, color_matrix_filter,
blur_filter, blur_filter,
glow_filter, glow_filter,
bevel_filter,
} }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
num_frames = 1
[image_comparison]
tolerance = 3
[player_options]
with_renderer = { optional = false, sample_count = 1 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
num_frames = 1
[image_comparison]
tolerance = 3
[player_options]
with_renderer = { optional = false, sample_count = 1 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
num_frames = 1
[image_comparison]
tolerance = 3
[player_options]
with_renderer = { optional = false, sample_count = 1 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
num_frames = 1
[image_comparison]
tolerance = 3
[player_options]
with_renderer = { optional = false, sample_count = 1 }