Files
wsg/lib/src/core/bloom.rs
T
Jérôme Bousquié 35aeb769a8 refactor examples
2026-09-25 10:19:24 +02:00

794 lines
29 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # Bloom Post-Process (Étape 23)
//!
//! Defines `BloomConfig` (public user-facing configuration) and the internal `BloomPipeline`
//! (GPU resources: half-res textures, blur/composite pipelines, bind groups). The bloom effect
//! is a 4-pass post-process that operates on the HDR texture before tone mapping:
//!
//! 1. **Threshold** (full → half res): extract pixels above a luminance threshold (soft-knee).
//! 2. **Blur H** (half res): horizontal separable Gaussian (9 taps).
//! 3. **Blur V** (half res): vertical separable Gaussian (9 taps).
//! 4. **Composite** (full res): `HDR += bloom × intensity`.
//!
//! The bloom is **opt-in** (`AppBuilder::with_bloom`) and only active when HDR is also enabled.
//! Without HDR, the values are already clamped to [0,1] and there is nothing "bright" to bloom.
use wgpu::{
BindGroup, BindGroupLayout, Buffer, BufferUsages, RenderPipeline, Sampler, Texture,
TextureUsages, TextureView,
};
/// User-facing bloom configuration (Étape 23).
///
/// Passed to `AppBuilder::with_bloom(config)` to enable the bloom post-process.
/// Can be updated at runtime via `App::set_bloom_config`.
#[derive(Debug, Clone)]
pub struct BloomConfig {
/// Luminance threshold (in linear HDR units). Pixels above this contribute to bloom.
/// Default: 1.0 (only overbright areas — emissives > 1.0, specular highlights).
pub threshold: f32,
/// Soft-knee width for the threshold ramp. Larger = smoother transition.
/// Default: 0.5.
pub knee: f32,
/// Bloom intensity (multiplier on the blurred result before adding to HDR).
/// Default: 0.8.
pub intensity: f32,
/// Blur radius in pixels (at half resolution). Larger = wider glow.
/// Default: 4.0.
pub radius: f32,
}
impl Default for BloomConfig {
fn default() -> Self {
Self {
threshold: 1.0,
knee: 0.5,
intensity: 0.8,
radius: 4.0,
}
}
}
/// Internal bloom pipeline state. Allocated when bloom + HDR are both active.
/// Recreated on resize.
pub(crate) struct BloomPipeline {
bright_texture: Texture,
bright_view: TextureView,
blur_texture: Texture,
blur_view: TextureView,
composite_texture: Texture,
composite_view: TextureView,
sampler: Sampler,
threshold_pipeline: RenderPipeline,
blur_pipeline: RenderPipeline,
composite_pipeline: RenderPipeline,
threshold_bg: BindGroup,
blur_bg_h: BindGroup,
blur_bg_v: BindGroup,
composite_bg: BindGroup,
threshold_uniform: Buffer,
blur_uniform_h: Buffer,
blur_uniform_v: Buffer,
composite_uniform: Buffer,
threshold_layout: BindGroupLayout,
blur_layout: BindGroupLayout,
composite_layout: BindGroupLayout,
half_w: u32,
half_h: u32,
width: u32,
height: u32,
}
impl BloomPipeline {
pub fn new(device: &wgpu::Device, width: u32, height: u32, hdr_view: &TextureView) -> Self {
let half_w = (width / 2).max(1);
let half_h = (height / 2).max(1);
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("bloom sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
..Default::default()
});
let (bright_texture, bright_view) =
create_bloom_texture(device, half_w, half_h, "bloom bright");
let (blur_texture, blur_view) = create_bloom_texture(device, half_w, half_h, "bloom blur");
let (composite_texture, composite_view) =
create_bloom_texture(device, width, height, "bloom composite");
// Bind group layouts.
let threshold_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bloom threshold bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: 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 blur_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bloom blur bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: 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 composite_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("bloom composite bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: 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,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
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: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
// Pipeline layouts.
let threshold_pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("bloom threshold pl"),
bind_group_layouts: &[Some(&threshold_layout)],
..Default::default()
});
let blur_pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("bloom blur pl"),
bind_group_layouts: &[Some(&blur_layout)],
..Default::default()
});
let composite_pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("bloom composite pl"),
bind_group_layouts: &[Some(&composite_layout)],
..Default::default()
});
// Shader modules.
let threshold_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("bloom threshold"),
source: wgpu::ShaderSource::Wgsl(
crate::utils::conf::BLOOM_THRESHOLD_SHADER.into(),
),
});
let blur_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("bloom blur"),
source: wgpu::ShaderSource::Wgsl(
crate::utils::conf::BLOOM_BLUR_SHADER.into(),
),
});
let composite_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("bloom composite"),
source: wgpu::ShaderSource::Wgsl(
crate::utils::conf::BLOOM_COMPOSITE_SHADER.into(),
),
});
// Shared fragment target state (all 3 passes output to Rgba16Float).
let fragment_targets = &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba16Float,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})];
// Threshold pipeline.
let threshold_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("bloom threshold pipeline"),
layout: Some(&threshold_pl),
vertex: wgpu::VertexState {
module: &threshold_module,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &threshold_module,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: fragment_targets,
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
});
// Blur pipeline.
let blur_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("bloom blur pipeline"),
layout: Some(&blur_pl),
vertex: wgpu::VertexState {
module: &blur_module,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blur_module,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: fragment_targets,
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
});
// Composite pipeline.
let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("bloom composite pipeline"),
layout: Some(&composite_pl),
vertex: wgpu::VertexState {
module: &composite_module,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &composite_module,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: fragment_targets,
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
});
// Uniform buffers (32 bytes each — WGSL uniform alignment requires padding;
// vec2 has align 8, vec3 has align 16, so structs are larger than their field sum).
let threshold_uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("bloom threshold uniform"),
size: 32,
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let blur_uniform_h = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("bloom blur H uniform"),
size: 32,
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let blur_uniform_v = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("bloom blur V uniform"),
size: 32,
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let composite_uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("bloom composite uniform"),
size: 32,
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Bind groups.
let threshold_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bloom threshold bg"),
layout: &threshold_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: threshold_uniform.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(hdr_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
let blur_bg_h = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bloom blur bg H"),
layout: &blur_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: blur_uniform_h.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&bright_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
let blur_bg_v = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bloom blur bg V"),
layout: &blur_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: blur_uniform_v.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&blur_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
let composite_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bloom composite bg"),
layout: &composite_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: composite_uniform.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(hdr_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&bright_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
Self {
bright_texture,
bright_view,
blur_texture,
blur_view,
composite_texture,
composite_view,
sampler,
threshold_pipeline,
blur_pipeline,
composite_pipeline,
threshold_bg,
blur_bg_h,
blur_bg_v,
composite_bg,
threshold_uniform,
blur_uniform_h,
blur_uniform_v,
composite_uniform,
threshold_layout,
blur_layout,
composite_layout,
half_w,
half_h,
width,
height,
}
}
pub fn resize(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
hdr_view: &TextureView,
) {
let half_w = (width / 2).max(1);
let half_h = (height / 2).max(1);
let (bright_texture, bright_view) =
create_bloom_texture(device, half_w, half_h, "bloom bright");
let (blur_texture, blur_view) = create_bloom_texture(device, half_w, half_h, "bloom blur");
let (composite_texture, composite_view) =
create_bloom_texture(device, width, height, "bloom composite");
self.threshold_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bloom threshold bg"),
layout: &self.threshold_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.threshold_uniform.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(hdr_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
self.blur_bg_h = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bloom blur bg H"),
layout: &self.blur_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.blur_uniform_h.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&bright_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
self.blur_bg_v = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bloom blur bg V"),
layout: &self.blur_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.blur_uniform_v.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&blur_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
self.composite_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bloom composite bg"),
layout: &self.composite_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.composite_uniform.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(hdr_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&bright_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
self.bright_texture = bright_texture;
self.bright_view = bright_view;
self.blur_texture = blur_texture;
self.blur_view = blur_view;
self.composite_texture = composite_texture;
self.composite_view = composite_view;
self.half_w = half_w;
self.half_h = half_h;
self.width = width;
self.height = height;
}
#[allow(dead_code)]
pub fn composite_view(&self) -> &TextureView {
&self.composite_view
}
pub fn composite_texture(&self) -> &Texture {
&self.composite_texture
}
pub fn record_passes(
&self,
encoder: &mut wgpu::CommandEncoder,
queue: &wgpu::Queue,
config: &BloomConfig,
) {
let threshold_data = [config.threshold, config.knee, 0.0, 0.0];
queue.write_buffer(
&self.threshold_uniform,
0,
bytemuck::cast_slice(&threshold_data),
);
let blur_h_data = [1.0 / self.half_w as f32, 0.0, config.radius, 0.0];
queue.write_buffer(&self.blur_uniform_h, 0, bytemuck::cast_slice(&blur_h_data));
let blur_v_data = [0.0, 1.0 / self.half_h as f32, config.radius, 0.0];
queue.write_buffer(&self.blur_uniform_v, 0, bytemuck::cast_slice(&blur_v_data));
let composite_data = [config.intensity, 0.0, 0.0, 0.0];
queue.write_buffer(
&self.composite_uniform,
0,
bytemuck::cast_slice(&composite_data),
);
// Pass 1: Threshold (HDR full → bright half)
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("bloom threshold"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.bright_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_viewport(
0.0,
0.0,
self.half_w as f32,
self.half_h as f32,
0.0,
1.0,
);
pass.set_pipeline(&self.threshold_pipeline);
pass.set_bind_group(0, &self.threshold_bg, &[]);
pass.draw(0..3, 0..1);
}
// Pass 2: Blur H (bright half → blur half)
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("bloom blur H"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.blur_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_viewport(
0.0,
0.0,
self.half_w as f32,
self.half_h as f32,
0.0,
1.0,
);
pass.set_pipeline(&self.blur_pipeline);
pass.set_bind_group(0, &self.blur_bg_h, &[]);
pass.draw(0..3, 0..1);
}
// Pass 3: Blur V (blur half → bright half)
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("bloom blur V"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.bright_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_viewport(
0.0,
0.0,
self.half_w as f32,
self.half_h as f32,
0.0,
1.0,
);
pass.set_pipeline(&self.blur_pipeline);
pass.set_bind_group(0, &self.blur_bg_v, &[]);
pass.draw(0..3, 0..1);
}
// Pass 4: Composite (HDR full + bright half → composite full)
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("bloom composite"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.composite_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_viewport(
0.0,
0.0,
self.width as f32,
self.height as f32,
0.0,
1.0,
);
pass.set_pipeline(&self.composite_pipeline);
pass.set_bind_group(0, &self.composite_bg, &[]);
pass.draw(0..3, 0..1);
}
}
}
fn create_bloom_texture(
device: &wgpu::Device,
width: u32,
height: u32,
label: &str,
) -> (Texture, TextureView) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba16Float,
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&Default::default());
(texture, view)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bloom_config_default() {
let cfg = BloomConfig::default();
assert_eq!(cfg.threshold, 1.0);
assert_eq!(cfg.knee, 0.5);
assert_eq!(cfg.intensity, 0.8);
assert_eq!(cfg.radius, 4.0);
}
#[test]
fn bloom_config_clone() {
let cfg = BloomConfig {
threshold: 2.0,
knee: 1.0,
intensity: 1.5,
radius: 6.0,
};
let cloned = cfg.clone();
assert_eq!(cloned.threshold, 2.0);
assert_eq!(cloned.intensity, 1.5);
}
}