use crate::draw2d::Color;
use crate::gpu::GpuContext;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct TransitionUniforms {
resolution: [f32; 2],
progress: f32,
_pad: f32,
color: [f32; 4],
}
pub struct TransitionPass {
fade_pipeline: wgpu::RenderPipeline,
crossfade_pipeline: wgpu::RenderPipeline,
uniform_buffer: wgpu::Buffer,
fade_bind_group_layout: wgpu::BindGroupLayout,
crossfade_bind_group_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
}
impl TransitionPass {
pub fn new(gpu: &GpuContext) -> Self {
let device = &gpu.device;
let fade_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Fade Transition Shader"),
source: wgpu::ShaderSource::Wgsl(FADE_SHADER.into()),
});
let crossfade_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Crossfade Transition Shader"),
source: wgpu::ShaderSource::Wgsl(CROSSFADE_SHADER.into()),
});
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Transition Uniforms"),
size: std::mem::size_of::<TransitionUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Transition Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let fade_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Fade Transition Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let crossfade_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Crossfade Transition Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let fade_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Fade Transition Pipeline Layout"),
bind_group_layouts: &[&fade_bind_group_layout],
push_constant_ranges: &[],
});
let fade_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Fade Transition Pipeline"),
layout: Some(&fade_pipeline_layout),
vertex: wgpu::VertexState {
module: &fade_shader,
entry_point: Some("vs"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &fade_shader,
entry_point: Some("fs"),
targets: &[Some(wgpu::ColorTargetState {
format: gpu.config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let crossfade_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Crossfade Transition Pipeline Layout"),
bind_group_layouts: &[&crossfade_bind_group_layout],
push_constant_ranges: &[],
});
let crossfade_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Crossfade Transition Pipeline"),
layout: Some(&crossfade_pipeline_layout),
vertex: wgpu::VertexState {
module: &crossfade_shader,
entry_point: Some("vs"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &crossfade_shader,
entry_point: Some("fs"),
targets: &[Some(wgpu::ColorTargetState {
format: gpu.config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
Self {
fade_pipeline,
crossfade_pipeline,
uniform_buffer,
fade_bind_group_layout,
crossfade_bind_group_layout,
sampler,
}
}
pub fn render_fade(
&self,
gpu: &GpuContext,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
scene_view: &wgpu::TextureView,
color: Color,
overlay_alpha: f32,
) {
let uniforms = TransitionUniforms {
resolution: [gpu.width() as f32, gpu.height() as f32],
progress: overlay_alpha,
_pad: 0.0,
color: [color.r, color.g, color.b, color.a],
};
gpu.queue
.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Fade Transition Bind Group"),
layout: &self.fade_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(scene_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Fade Transition Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.fade_pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
pub fn render_crossfade(
&self,
gpu: &GpuContext,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
old_scene_view: &wgpu::TextureView,
new_scene_view: &wgpu::TextureView,
blend: f32,
) {
let uniforms = TransitionUniforms {
resolution: [gpu.width() as f32, gpu.height() as f32],
progress: blend,
_pad: 0.0,
color: [0.0, 0.0, 0.0, 0.0], };
gpu.queue
.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Crossfade Transition Bind Group"),
layout: &self.crossfade_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(old_scene_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(new_scene_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Crossfade Transition Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.crossfade_pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
const FADE_SHADER: &str = r#"
struct Uniforms {
resolution: vec2f,
progress: f32,
_pad: f32,
color: vec4f,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var scene_texture: texture_2d<f32>;
@group(0) @binding(2) var scene_sampler: sampler;
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) uv: vec2f,
}
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VertexOutput {
// Fullscreen triangle (oversized to cover entire clip space)
// vi=0: (-1, -1), vi=1: (3, -1), vi=2: (-1, 3)
var out: VertexOutput;
let x = f32(i32(vi & 1u) * 4 - 1);
let y = f32(i32(vi & 2u) * 2 - 1);
out.position = vec4f(x, y, 0.0, 1.0);
// UV coordinates: map clip space to [0,1]
out.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);
return out;
}
@fragment
fn fs(in: VertexOutput) -> @location(0) vec4f {
let scene = textureSample(scene_texture, scene_sampler, in.uv);
// Blend scene with overlay color based on progress
// progress = 0: full scene, progress = 1: full overlay color
return mix(scene, u.color, u.progress);
}
"#;
const CROSSFADE_SHADER: &str = r#"
struct Uniforms {
resolution: vec2f,
progress: f32,
_pad: f32,
color: vec4f, // Not used, but keeps struct consistent
}
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var old_texture: texture_2d<f32>;
@group(0) @binding(2) var new_texture: texture_2d<f32>;
@group(0) @binding(3) var tex_sampler: sampler;
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) uv: vec2f,
}
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VertexOutput {
// Fullscreen triangle (oversized to cover entire clip space)
// vi=0: (-1, -1), vi=1: (3, -1), vi=2: (-1, 3)
var out: VertexOutput;
let x = f32(i32(vi & 1u) * 4 - 1);
let y = f32(i32(vi & 2u) * 2 - 1);
out.position = vec4f(x, y, 0.0, 1.0);
// UV coordinates: map clip space to [0,1]
out.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);
return out;
}
@fragment
fn fs(in: VertexOutput) -> @location(0) vec4f {
let old_scene = textureSample(old_texture, tex_sampler, in.uv);
let new_scene = textureSample(new_texture, tex_sampler, in.uv);
// Crossfade: blend from old to new based on progress
return mix(old_scene, new_scene, u.progress);
}
"#;