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..7cac8cc8 100644 --- a/apps/web/src/lib/effects/definitions/index.ts +++ b/apps/web/src/lib/effects/definitions/index.ts @@ -1,7 +1,21 @@ import { effectsRegistry } from "../registry"; import { blurEffectDefinition } from "./blur"; +import { brightnessContrastEffectDefinition } from "./brightness-contrast"; +import { grayscaleEffectDefinition } from "./grayscale"; +import { invertEffectDefinition } from "./invert"; +import { saturationEffectDefinition } from "./saturation"; +import { sepiaEffectDefinition } from "./sepia"; +import { vignetteEffectDefinition } from "./vignette"; -const defaultEffects = [blurEffectDefinition]; +const defaultEffects = [ + blurEffectDefinition, + brightnessContrastEffectDefinition, + grayscaleEffectDefinition, + saturationEffectDefinition, + sepiaEffectDefinition, + invertEffectDefinition, + vignetteEffectDefinition, +]; export function registerDefaultEffects(): void { for (const definition of defaultEffects) { diff --git a/apps/web/src/lib/effects/definitions/invert.ts b/apps/web/src/lib/effects/definitions/invert.ts new file mode 100644 index 00000000..e2db9744 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/invert.ts @@ -0,0 +1,33 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const INVERT_SHADER = "invert"; + +export const invertEffectDefinition: EffectDefinition = { + type: "invert", + name: "Invert", + keywords: ["invert", "negative", "reverse", "opposite"], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 100, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: INVERT_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/saturation.ts b/apps/web/src/lib/effects/definitions/saturation.ts new file mode 100644 index 00000000..abfe2f05 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/saturation.ts @@ -0,0 +1,33 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const SATURATION_SHADER = "saturation"; + +export const saturationEffectDefinition: EffectDefinition = { + type: "saturation", + name: "Saturation", + keywords: ["saturation", "vibrance", "vivid", "color", "desaturate"], + params: [ + { + key: "saturation", + label: "Saturation", + type: "number", + default: 100, + min: 0, + max: 300, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: SATURATION_SHADER, + uniforms: ({ effectParams }) => { + const raw = effectParams.saturation; + const saturation = + typeof raw === "number" ? raw / 100 : 1; + return { u_saturation: saturation }; + }, + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/sepia.ts b/apps/web/src/lib/effects/definitions/sepia.ts new file mode 100644 index 00000000..64f03249 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/sepia.ts @@ -0,0 +1,33 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const SEPIA_SHADER = "sepia"; + +export const sepiaEffectDefinition: EffectDefinition = { + type: "sepia", + name: "Sepia", + keywords: ["sepia", "vintage", "warm", "old", "retro", "antique"], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 100, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: SEPIA_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/vignette.ts b/apps/web/src/lib/effects/definitions/vignette.ts new file mode 100644 index 00000000..a9d035ef --- /dev/null +++ b/apps/web/src/lib/effects/definitions/vignette.ts @@ -0,0 +1,48 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const VIGNETTE_SHADER = "vignette"; + +export const vignetteEffectDefinition: EffectDefinition = { + type: "vignette", + name: "Vignette", + keywords: ["vignette", "dark edges", "focus", "spotlight", "center"], + params: [ + { + key: "radius", + label: "Radius", + type: "number", + default: 75, + min: 10, + max: 100, + step: 1, + }, + { + key: "softness", + label: "Softness", + type: "number", + default: 35, + min: 5, + max: 80, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: VIGNETTE_SHADER, + uniforms: ({ effectParams }) => { + const rawRadius = effectParams.radius; + const rawSoftness = effectParams.softness; + const radius = + (typeof rawRadius === "number" ? rawRadius : 75) / + 100; + const softness = + (typeof rawSoftness === "number" + ? rawSoftness + : 35) / 100; + return { u_radius: radius, u_softness: softness }; + }, + }, + ], + }, +}; diff --git a/rust/crates/effects/src/pipeline.rs b/rust/crates/effects/src/pipeline.rs index 1ea49053..c189a28a 100644 --- a/rust/crates/effects/src/pipeline.rs +++ b/rust/crates/effects/src/pipeline.rs @@ -7,8 +7,45 @@ 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, + ), + ( + "saturation", + include_str!("shaders/saturation.wgsl"), + pack_saturation as UniformPacker, + ), + ( + "sepia", + include_str!("shaders/sepia.wgsl"), + pack_sepia as UniformPacker, + ), + ( + "invert", + include_str!("shaders/invert.wgsl"), + pack_invert as UniformPacker, + ), + ( + "vignette", + include_str!("shaders/vignette.wgsl"), + pack_vignette as UniformPacker, + ), +]; pub struct ApplyEffectsOptions<'a> { pub source: &'a wgpu::Texture, @@ -40,16 +77,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 +117,7 @@ impl EffectPipeline { count: None, }], }); + let vertex_shader_module = context .device() @@ -77,13 +125,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 +137,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 +258,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 +316,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 +347,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 +380,103 @@ 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) +} + +fn pack_saturation( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let saturation = read_number_uniform_or(pass, "u_saturation", 1.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = saturation; + Ok(buf) +} + +fn pack_sepia( + 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) +} + +fn pack_invert( + 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) +} + +fn pack_vignette( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let radius = read_number_uniform_or(pass, "u_radius", 0.75); + let softness = read_number_uniform_or(pass, "u_softness", 0.35); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = radius; + buf.scalars[0][1] = softness; + 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); +} diff --git a/rust/crates/effects/src/shaders/invert.wgsl b/rust/crates/effects/src/shaders/invert.wgsl new file mode 100644 index 00000000..3c1acc5d --- /dev/null +++ b/rust/crates/effects/src/shaders/invert.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 intensity = uniforms.scalars[0].x; + + let inverted = vec3f(1.0 - color.r, 1.0 - color.g, 1.0 - color.b); + let result = mix(color.rgb, inverted, intensity); + + return vec4f(result, color.a); +} diff --git a/rust/crates/effects/src/shaders/saturation.wgsl b/rust/crates/effects/src/shaders/saturation.wgsl new file mode 100644 index 00000000..a042da89 --- /dev/null +++ b/rust/crates/effects/src/shaders/saturation.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 saturation = uniforms.scalars[0].x; + + let luminance = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722)); + let gray = vec3f(luminance); + + let result = mix(gray, color.rgb, saturation); + + return vec4f(result, color.a); +} diff --git a/rust/crates/effects/src/shaders/sepia.wgsl b/rust/crates/effects/src/shaders/sepia.wgsl new file mode 100644 index 00000000..1118e46b --- /dev/null +++ b/rust/crates/effects/src/shaders/sepia.wgsl @@ -0,0 +1,30 @@ +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 r = min(color.r * 0.393 + color.g * 0.769 + color.b * 0.189, 1.0); + let g = min(color.r * 0.349 + color.g * 0.686 + color.b * 0.168, 1.0); + let b = min(color.r * 0.272 + color.g * 0.534 + color.b * 0.131, 1.0); + + let sepia = vec3f(r, g, b); + let result = mix(color.rgb, sepia, intensity); + + return vec4f(result, color.a); +} diff --git a/rust/crates/effects/src/shaders/vignette.wgsl b/rust/crates/effects/src/shaders/vignette.wgsl new file mode 100644 index 00000000..5dad804d --- /dev/null +++ b/rust/crates/effects/src/shaders/vignette.wgsl @@ -0,0 +1,29 @@ +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 radius = uniforms.scalars[0].x; + let softness = uniforms.scalars[0].y; + + let center = input.tex_coord - vec2f(0.5); + let dist = length(center); + + let edge = smoothstep(radius, radius - softness, dist); + + return vec4f(color.rgb * edge, color.a); +}