diff --git a/apps/web/src/lib/effects/definitions/brightness-contrast.ts b/apps/web/src/lib/effects/definitions/brightness-contrast.ts new file mode 100644 index 00000000..47a263aa --- /dev/null +++ b/apps/web/src/lib/effects/definitions/brightness-contrast.ts @@ -0,0 +1,46 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const BRIGHTNESS_CONTRAST_SHADER = "brightness-contrast"; + +export const brightnessContrastEffectDefinition: EffectDefinition = { + type: "brightness-contrast", + name: "Brightness / Contrast", + keywords: ["brightness", "contrast", "light", "dark", "exposure"], + params: [ + { + key: "brightness", + label: "Brightness", + type: "number", + default: 0, + min: -1, + max: 1, + step: 0.01, + }, + { + key: "contrast", + label: "Contrast", + type: "number", + default: 1, + min: 0, + max: 3, + step: 0.01, + }, + ], + renderer: { + passes: [ + { + shader: BRIGHTNESS_CONTRAST_SHADER, + uniforms: ({ effectParams }) => ({ + u_brightness: + typeof effectParams.brightness === "number" + ? effectParams.brightness + : 0, + u_contrast: + typeof effectParams.contrast === "number" + ? effectParams.contrast + : 1, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/grayscale.ts b/apps/web/src/lib/effects/definitions/grayscale.ts new file mode 100644 index 00000000..d96d4a52 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/grayscale.ts @@ -0,0 +1,33 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const GRAYSCALE_SHADER = "grayscale"; + +export const grayscaleEffectDefinition: EffectDefinition = { + type: "grayscale", + name: "Grayscale", + keywords: ["grayscale", "grey", "black and white", "monochrome", "desaturate"], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 100, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: GRAYSCALE_SHADER, + uniforms: ({ effectParams }) => { + const raw = effectParams.intensity; + const intensity = + typeof raw === "number" ? raw / 100 : 1; + return { u_intensity: intensity }; + }, + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/index.ts b/apps/web/src/lib/effects/definitions/index.ts index edc8c5e2..523164d4 100644 --- a/apps/web/src/lib/effects/definitions/index.ts +++ b/apps/web/src/lib/effects/definitions/index.ts @@ -1,7 +1,13 @@ import { effectsRegistry } from "../registry"; import { blurEffectDefinition } from "./blur"; +import { brightnessContrastEffectDefinition } from "./brightness-contrast"; +import { grayscaleEffectDefinition } from "./grayscale"; -const defaultEffects = [blurEffectDefinition]; +const defaultEffects = [ + blurEffectDefinition, + brightnessContrastEffectDefinition, + grayscaleEffectDefinition, +]; export function registerDefaultEffects(): void { for (const definition of defaultEffects) { diff --git a/rust/crates/effects/src/pipeline.rs b/rust/crates/effects/src/pipeline.rs index 1ea49053..fa73bff9 100644 --- a/rust/crates/effects/src/pipeline.rs +++ b/rust/crates/effects/src/pipeline.rs @@ -7,8 +7,25 @@ use wgpu::util::DeviceExt; use crate::{EffectPass, UniformValue}; -const GAUSSIAN_BLUR_SHADER_ID: &str = "gaussian-blur"; -const GAUSSIAN_BLUR_SHADER_SOURCE: &str = include_str!("shaders/gaussian_blur.wgsl"); +type UniformPacker = fn(&EffectPass, u32, u32) -> Result; + +const SHADER_REGISTRY: &[(&str, &str, UniformPacker)] = &[ + ( + "gaussian-blur", + include_str!("shaders/gaussian_blur.wgsl"), + pack_gaussian_blur as UniformPacker, + ), + ( + "brightness-contrast", + include_str!("shaders/brightness_contrast.wgsl"), + pack_brightness_contrast as UniformPacker, + ), + ( + "grayscale", + include_str!("shaders/grayscale.wgsl"), + pack_grayscale as UniformPacker, + ), +]; pub struct ApplyEffectsOptions<'a> { pub source: &'a wgpu::Texture, @@ -40,16 +57,26 @@ pub enum EffectsError { uniform: String, expected_length: usize, }, - #[error("Shader '{shader}' does not support uniform '{uniform}'")] - UnsupportedUniform { shader: String, uniform: String }, } #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] -struct EffectUniformBuffer { +struct GenericUniformBuffer { resolution: [f32; 2], - direction: [f32; 2], - scalars: [f32; 4], + _pad_res: [f32; 2], + scalars: [[f32; 4]; 2], + vectors: [[f32; 4]; 2], +} + +impl Default for GenericUniformBuffer { + fn default() -> Self { + Self { + resolution: [0.0, 0.0], + _pad_res: [0.0, 0.0], + scalars: [[0.0; 4]; 2], + vectors: [[0.0; 4]; 2], + } + } } impl EffectPipeline { @@ -70,6 +97,7 @@ impl EffectPipeline { count: None, }], }); + let vertex_shader_module = context .device() @@ -77,13 +105,7 @@ impl EffectPipeline { label: Some("effects-fullscreen-shader"), source: wgpu::ShaderSource::Wgsl(FULLSCREEN_SHADER_SOURCE.into()), }); - let gaussian_blur_shader_module = - context - .device() - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("effects-gaussian-blur-shader"), - source: wgpu::ShaderSource::Wgsl(GAUSSIAN_BLUR_SHADER_SOURCE.into()), - }); + let pipeline_layout = context .device() @@ -95,44 +117,54 @@ impl EffectPipeline { ], immediate_size: 0, }); - let gaussian_blur_pipeline = - context - .device() - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("effects-gaussian-blur-pipeline"), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &vertex_shader_module, - entry_point: Some("vertex_main"), - buffers: &[wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::<[f32; 2]>() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[wgpu::VertexAttribute { - format: wgpu::VertexFormat::Float32x2, - offset: 0, - shader_location: 0, + + let mut pipelines = HashMap::new(); + for (id, source, _packer) in SHADER_REGISTRY { + let shader_module = + context + .device() + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(&format!("effects-{id}-shader")), + source: wgpu::ShaderSource::Wgsl((*source).into()), + }); + let pipeline = + context + .device() + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(&format!("effects-{id}-pipeline")), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vertex_shader_module, + entry_point: Some("vertex_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<[f32; 2]>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 0, + }], }], - }], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &gaussian_blur_shader_module, - entry_point: Some("fragment_main"), - targets: &[Some(wgpu::ColorTargetState { - format: context.texture_format(), - blend: None, - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview_mask: None, - cache: None, - }); - let pipelines = - HashMap::from([(GAUSSIAN_BLUR_SHADER_ID.to_string(), gaussian_blur_pipeline)]); + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: Some("fragment_main"), + targets: &[Some(wgpu::ColorTargetState { + format: context.texture_format(), + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + pipelines.insert(id.to_string(), pipeline); + } Self { uniform_bind_group_layout, @@ -206,12 +238,14 @@ impl EffectPipeline { }, ], }); + + let packed = pack_uniforms_for_shader(pass, width, height)?; let uniform_buffer = context .device() .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("effects-uniform-buffer"), - contents: bytemuck::bytes_of(&pack_effect_uniforms(pass, width, height)?), + contents: bytemuck::bytes_of(&packed), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); let uniform_bind_group = @@ -262,30 +296,18 @@ impl EffectPipeline { } } -fn pack_effect_uniforms( +fn pack_uniforms_for_shader( pass: &EffectPass, width: u32, height: u32, -) -> Result { - let shader = pass.shader.as_str(); - let sigma = read_number_uniform(pass, "u_sigma")?; - let step = read_number_uniform(pass, "u_step")?; - let direction = read_vec2_uniform(pass, "u_direction")?; - - for uniform in pass.uniforms.keys() { - if uniform == "u_sigma" || uniform == "u_step" || uniform == "u_direction" { - continue; +) -> Result { + for (id, _source, packer) in SHADER_REGISTRY { + if pass.shader == *id { + return packer(pass, width, height); } - return Err(EffectsError::UnsupportedUniform { - shader: shader.to_string(), - uniform: uniform.clone(), - }); } - - Ok(EffectUniformBuffer { - resolution: [width as f32, height as f32], - direction, - scalars: [sigma, step, 0.0, 0.0], + Err(EffectsError::UnknownEffectShader { + shader: pass.shader.clone(), }) } @@ -305,6 +327,16 @@ fn read_number_uniform(pass: &EffectPass, uniform: &str) -> Result f32 { + pass.uniforms + .get(uniform) + .and_then(|v| match v { + UniformValue::Number(n) => Some(*n), + _ => None, + }) + .unwrap_or(default) +} + fn read_vec2_uniform(pass: &EffectPass, uniform: &str) -> Result<[f32; 2], EffectsError> { let Some(value) = pass.uniforms.get(uniform) else { return Err(EffectsError::MissingUniform { @@ -328,3 +360,49 @@ fn read_vec2_uniform(pass: &EffectPass, uniform: &str) -> Result<[f32; 2], Effec } Ok([values[0], values[1]]) } + +fn pack_gaussian_blur( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let sigma = read_number_uniform(pass, "u_sigma")?; + let step = read_number_uniform(pass, "u_step")?; + let direction = read_vec2_uniform(pass, "u_direction")?; + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = sigma; + buf.scalars[0][1] = step; + buf.vectors[0][0] = direction[0]; + buf.vectors[0][1] = direction[1]; + Ok(buf) +} + +fn pack_brightness_contrast( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let brightness = read_number_uniform_or(pass, "u_brightness", 0.0); + let contrast = read_number_uniform_or(pass, "u_contrast", 1.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = brightness; + buf.scalars[0][1] = contrast; + Ok(buf) +} + +fn pack_grayscale( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 1.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + Ok(buf) +} diff --git a/rust/crates/effects/src/shaders/brightness_contrast.wgsl b/rust/crates/effects/src/shaders/brightness_contrast.wgsl new file mode 100644 index 00000000..59f36ae4 --- /dev/null +++ b/rust/crates/effects/src/shaders/brightness_contrast.wgsl @@ -0,0 +1,26 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let brightness = uniforms.scalars[0].x; + let contrast = uniforms.scalars[0].y; + + let adjusted = (color.rgb - 0.5) * contrast + 0.5 + brightness; + + return vec4f(clamp(adjusted, vec3f(0.0), vec3f(1.0)), color.a); +} diff --git a/rust/crates/effects/src/shaders/gaussian_blur.wgsl b/rust/crates/effects/src/shaders/gaussian_blur.wgsl index 6c8d311d..1ab45d45 100644 --- a/rust/crates/effects/src/shaders/gaussian_blur.wgsl +++ b/rust/crates/effects/src/shaders/gaussian_blur.wgsl @@ -5,8 +5,9 @@ struct VertexOutput { struct EffectUniforms { resolution: vec2f, - direction: vec2f, - scalars: vec4f, + _pad_res: vec2f, + scalars: array, + vectors: array, } @group(0) @binding(0) var input_texture: texture_2d; @@ -16,8 +17,9 @@ struct EffectUniforms { @fragment fn fragment_main(input: VertexOutput) -> @location(0) vec4f { let texel_size = vec2f(1.0, 1.0) / uniforms.resolution; - let sigma = uniforms.scalars.x; - let step_size = uniforms.scalars.y; + let sigma = uniforms.scalars[0].x; + let step_size = uniforms.scalars[0].y; + let direction = uniforms.vectors[0].xy; var color = vec4f(0.0, 0.0, 0.0, 0.0); var total_weight = 0.0; @@ -25,7 +27,7 @@ fn fragment_main(input: VertexOutput) -> @location(0) vec4f { for (var index = -30; index <= 30; index = index + 1) { let position = f32(index) * step_size; let weight = exp(-(position * position) / (2.0 * sigma * sigma)); - let sample_uv = input.tex_coord + (texel_size * uniforms.direction * position); + let sample_uv = input.tex_coord + (texel_size * direction * position); color = color + textureSample(input_texture, input_sampler, sample_uv) * weight; total_weight = total_weight + weight; } diff --git a/rust/crates/effects/src/shaders/grayscale.wgsl b/rust/crates/effects/src/shaders/grayscale.wgsl new file mode 100644 index 00000000..5ece841a --- /dev/null +++ b/rust/crates/effects/src/shaders/grayscale.wgsl @@ -0,0 +1,28 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let intensity = uniforms.scalars[0].x; + + let luminance = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722)); + let gray = vec3f(luminance); + + let result = mix(color.rgb, gray, intensity); + + return vec4f(result, color.a); +}