Skip to content

Commit 2317f7b

Browse files
Implements post-process lens distortion effects (#23110)
# Objective - Implements post-process lens distortion effects based on `EffectStackPlugin`. ## Solution - This PR's implementation is based on a simplified special case of the Brown-Conrady model, where p₁ = p₂ = 0 and control is retained only for k₁ and k₂. - Additionally, supports controlling the degree of distortion in the horizontal and vertical directions based on the direction vector. ## Testing - These effect components can work independently. - The example works as expected. - CI --- ## Showcase - Barrel distortion <img width="1918" height="1104" alt="tu" src="https://github.com/user-attachments/assets/6d1b1660-0892-4433-a152-4ac277b9382c" /> - Pincushion distortion <img width="1935" height="1084" alt="ao" src="https://github.com/user-attachments/assets/2d3c5373-43a8-427c-bd0a-59ccce4448fc" /> - Pincushion distortion with no distortion on the x-axis. <img width="1921" height="1089" alt="ao_no_x" src="https://github.com/user-attachments/assets/c1b4b55d-7b7f-4cb7-8459-debe4cf2f283" /> - With all effects. <img width="2559" height="1314" alt="cur" src="https://github.com/user-attachments/assets/724cfa30-9b36-42b9-a480-0d96720f760b" />
1 parent 90a45fc commit 2317f7b

6 files changed

Lines changed: 297 additions & 13 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use bevy_ecs::{
2+
component::Component,
3+
query::{QueryItem, With},
4+
system::lifetimeless::Read,
5+
};
6+
use bevy_math::{ops::abs, Vec2};
7+
use bevy_reflect::Reflect;
8+
use bevy_render::{
9+
extract_component::ExtractComponent, render_resource::ShaderType, sync_component::SyncComponent,
10+
};
11+
12+
/// Simulates the warping of the image caused by real-world camera lenses.
13+
///
14+
/// [Lens distortion] simulates the imperfections of optical systems, where
15+
/// straight lines in the real world appear curved in the image. This effect
16+
/// is commonly used to create a sense of unease or disorientation, to mimic
17+
/// specific camera equipment, or to enhance the scale and immersion of scenes.
18+
///
19+
/// Bevy's implementation is based on a simplified special case of the
20+
/// Brown-Conrady model, where p₁ = p₂ = 0 and control is retained only
21+
/// for k1 and k2.
22+
#[derive(Reflect, Component, Clone)]
23+
pub struct LensDistortion {
24+
/// The overall strength of the distortion effect.
25+
///
26+
/// Positive values typically produce **barrel distortion** (bulging outwards),
27+
/// while negative values produce **pincushion distortion** (pinching inwards).
28+
/// This corresponds roughly to the radial distortion coefficient `k1`
29+
/// in the simplified model.
30+
///
31+
/// The default value is 0.5.
32+
pub intensity: f32,
33+
/// A global scale factor applied to the final distorted image.
34+
///
35+
/// Strong distortion pushes pixels away from the center or pulls them in,
36+
/// resulting in visible **stretching artifacts** at the screen edges.
37+
/// Increasing this value zooms in to **crop out** these extreme edge artifacts,
38+
/// ensuring the screen remains fully covered at the cost of a tighter field of view.
39+
///
40+
/// The default value is 1.0(No zoom).
41+
pub scale: f32,
42+
/// A multiplier that determines how the distortion scales along the X and Y axes.
43+
///
44+
/// By default, this should be `Vec2::ONE` for uniform radial distortion.
45+
/// Modifying these values allows for anamorphic-like effects where the distortion
46+
/// is stronger on one axis than the other. When a component of multiplier is set to 0.0,
47+
/// no distortion effect is applied.
48+
///
49+
/// The default value is `Vec2::ONE`
50+
pub multiplier: Vec2,
51+
/// The center point of the distortion effect in UV space `[0.0, 1.0]`.
52+
///
53+
/// Distortion radiates outward or inward from this point.
54+
///
55+
/// The default value is `Vec2::splat(0.5)`
56+
pub center: Vec2,
57+
/// Controls the sharpness of the distortion curve near the screen edges.
58+
///
59+
/// `edge_curvature` provides indirect control over the k2 parameter.
60+
/// The reason for indirect control is that k1 and k2 are typically correlated.
61+
/// If k2 did not vary with k1, it would easily cause visual jumping when intensity
62+
/// transitions from positive to negative.
63+
/// For a simple and natural look in most cases, we recommend setting `edge_curvature` to 0.0.
64+
///
65+
/// The default value is 0.0.
66+
pub edge_curvature: f32,
67+
}
68+
69+
impl Default for LensDistortion {
70+
fn default() -> Self {
71+
Self {
72+
intensity: 0.5,
73+
scale: 1.0,
74+
multiplier: Vec2::ONE,
75+
center: Vec2::splat(0.5),
76+
edge_curvature: 0.0,
77+
}
78+
}
79+
}
80+
81+
impl SyncComponent for LensDistortion {
82+
type Target = Self;
83+
}
84+
85+
impl ExtractComponent for LensDistortion {
86+
type QueryData = Read<LensDistortion>;
87+
type QueryFilter = With<LensDistortion>;
88+
type Out = Self;
89+
90+
fn extract_component(lens_distortion: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
91+
// Skip the postprocessing phase entirely if the intensity is negligible.
92+
if abs(lens_distortion.intensity) > 1e-4 {
93+
Some(lens_distortion.clone())
94+
} else {
95+
None
96+
}
97+
}
98+
}
99+
100+
/// The on-GPU version of the [`LensDistortion`] settings.
101+
///
102+
/// See the documentation for [`LensDistortion`] for more information on
103+
/// each of these fields.
104+
#[derive(ShaderType, Default)]
105+
pub struct LensDistortionUniform {
106+
pub(super) intensity: f32,
107+
pub(super) scale: f32,
108+
pub(super) multiplier: Vec2,
109+
pub(super) center: Vec2,
110+
pub(super) edge_curvature: f32,
111+
pub(super) unused: u32,
112+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// The lens distortion postprocessing effect.
2+
3+
#define_import_path bevy_post_process::effect_stack::lens_distortion
4+
5+
// See `bevy_post_process::effect_stack::LensDistortion` for more
6+
// information on these fields.
7+
struct LensDistortionSettings {
8+
intensity: f32,
9+
scale: f32,
10+
multiplier: vec2<f32>,
11+
center: vec2<f32>,
12+
edge_curvature: f32,
13+
unused: u32,
14+
}
15+
16+
const VISUAL_THRESHOLD: f32 = 1e-4;
17+
const MATH_EPSILON: f32 = 1e-6;
18+
19+
// The settings supplied by the developer.
20+
@group(0) @binding(6) var<uniform> lens_distortion_settings: LensDistortionSettings;
21+
22+
fn lens_distortion(uv: vec2<f32>) -> vec2<f32>{
23+
let intensity = lens_distortion_settings.intensity;
24+
if (abs(intensity) < VISUAL_THRESHOLD) {
25+
return uv;
26+
}
27+
let multiplier = lens_distortion_settings.multiplier;
28+
let center = lens_distortion_settings.center;
29+
30+
let uv_centered = uv - center;
31+
// Prevent division by zero.
32+
let radius = max(length(uv_centered), MATH_EPSILON);
33+
34+
let direction = uv_centered / radius;
35+
let adjust = dot(abs(direction), multiplier);
36+
37+
// Maintains the correlation between k2 and k1, while ensuring the sign of k2
38+
// is determined solely by `edge_curvature` rather than being influenced by intensity.
39+
let k1 = intensity * adjust;
40+
let k2 = k1 * intensity * lens_distortion_settings.edge_curvature;
41+
42+
let r2 = radius * radius;
43+
let r_distorted = radius * (1.0 + (k1 + k2 * r2) * r2);
44+
45+
let uv_distorted = direction * r_distorted + center;
46+
47+
// Compensates for the distortion pushing pixels outside the [0,1] UV bounds.
48+
let uv_scaled = (uv_distorted - center) / lens_distortion_settings.scale + center;
49+
50+
// Discard out-of-bounds pixels to prevent edge bleeding artifacts.
51+
let uv_safe = clamp(uv_scaled, vec2<f32>(0.0), vec2<f32>(1.0));
52+
53+
return uv_safe;
54+
}

crates/bevy_post_process/src/effect_stack/mod.rs

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
//! Includes:
44
//!
55
//! - Chromatic Aberration
6+
//! - Lens Distortion
67
//! - Vignette
78
89
mod chromatic_aberration;
10+
mod lens_distortion;
911
mod vignette;
1012

1113
use bevy_color::ColorToComponents;
14+
use bevy_math::Vec2;
1215
pub use chromatic_aberration::{ChromaticAberration, ChromaticAberrationUniform};
16+
pub use lens_distortion::{LensDistortion, LensDistortionUniform};
1317
pub use vignette::{Vignette, VignetteUniform};
1418

1519
use crate::effect_stack::chromatic_aberration::{
@@ -65,6 +69,7 @@ use bevy_core_pipeline::{
6569
/// Includes:
6670
///
6771
/// - Chromatic Aberration
72+
/// - Lens Distortion
6873
/// - Vignette
6974
#[derive(Default)]
7075
pub struct EffectStackPlugin;
@@ -107,6 +112,7 @@ pub struct PostProcessingPipelineId(CachedRenderPipelineId);
107112
pub struct PostProcessingUniformBuffers {
108113
chromatic_aberration: DynamicUniformBuffer<ChromaticAberrationUniform>,
109114
vignette: DynamicUniformBuffer<VignetteUniform>,
115+
lens_distortion: DynamicUniformBuffer<LensDistortionUniform>,
110116
}
111117

112118
/// A component, part of the render world, that stores the appropriate byte
@@ -116,11 +122,13 @@ pub struct PostProcessingUniformBuffers {
116122
pub struct PostProcessingUniformBufferOffsets {
117123
chromatic_aberration: u32,
118124
vignette: u32,
125+
lens_distortion: u32,
119126
}
120127

121128
impl Plugin for EffectStackPlugin {
122129
fn build(&self, app: &mut App) {
123130
load_shader_library!(app, "chromatic_aberration.wgsl");
131+
load_shader_library!(app, "lens_distortion.wgsl");
124132
load_shader_library!(app, "vignette.wgsl");
125133

126134
embedded_asset!(app, "post_process.wgsl");
@@ -140,6 +148,7 @@ impl Plugin for EffectStackPlugin {
140148
));
141149

142150
app.add_plugins(ExtractComponentPlugin::<ChromaticAberration>::default())
151+
.add_plugins(ExtractComponentPlugin::<LensDistortion>::default())
143152
.add_plugins(ExtractComponentPlugin::<Vignette>::default());
144153

145154
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
@@ -191,6 +200,8 @@ pub fn init_post_processing_pipeline(
191200
uniform_buffer::<ChromaticAberrationUniform>(true),
192201
// Vignette settings:
193202
uniform_buffer::<VignetteUniform>(true),
203+
// Lens Distortion settings:
204+
uniform_buffer::<LensDistortionUniform>(true),
194205
),
195206
),
196207
);
@@ -247,7 +258,7 @@ pub(crate) fn post_processing(
247258
view: ViewQuery<(
248259
&ViewTarget,
249260
&PostProcessingPipelineId,
250-
AnyOf<(&ChromaticAberration, &Vignette)>,
261+
AnyOf<(&ChromaticAberration, &Vignette, &LensDistortion)>,
251262
&PostProcessingUniformBufferOffsets,
252263
)>,
253264
pipeline_cache: Res<PipelineCache>,
@@ -260,9 +271,12 @@ pub(crate) fn post_processing(
260271
let (view_target, pipeline_id, post_effects, post_processing_uniform_buffer_offsets) =
261272
view.into_inner();
262273

263-
let (maybe_chromatic_aberration, maybe_vignette) = post_effects;
274+
let (maybe_chromatic_aberration, maybe_vignette, maybe_lens_distortion) = post_effects;
264275

265-
if maybe_chromatic_aberration.is_none() && maybe_vignette.is_none() {
276+
if maybe_chromatic_aberration.is_none()
277+
&& maybe_vignette.is_none()
278+
&& maybe_lens_distortion.is_none()
279+
{
266280
return;
267281
}
268282

@@ -293,6 +307,12 @@ pub(crate) fn post_processing(
293307
return;
294308
};
295309

310+
let Some(lens_distortion_uniform_buffer_binding) =
311+
post_processing_uniform_buffers.lens_distortion.binding()
312+
else {
313+
return;
314+
};
315+
296316
// Use the [`PostProcessWrite`] infrastructure, since this is a full-screen pass.
297317
let post_process = view_target.post_process_write();
298318

@@ -320,6 +340,7 @@ pub(crate) fn post_processing(
320340
&post_processing_pipeline.chromatic_aberration_lut_sampler,
321341
chromatic_aberration_uniform_buffer_binding,
322342
vignette_uniform_buffer_binding,
343+
lens_distortion_uniform_buffer_binding,
323344
)),
324345
);
325346

@@ -336,6 +357,7 @@ pub(crate) fn post_processing(
336357
&[
337358
post_processing_uniform_buffer_offsets.chromatic_aberration,
338359
post_processing_uniform_buffer_offsets.vignette,
360+
post_processing_uniform_buffer_offsets.lens_distortion,
339361
],
340362
);
341363
render_pass.draw(0..3, 0..1);
@@ -354,6 +376,7 @@ pub fn prepare_post_processing_pipelines(
354376
Or<(
355377
With<ChromaticAberration>,
356378
With<Vignette>,
379+
With<LensDistortion>,
357380
With<ExtractedCamera>,
358381
)>,
359382
>,
@@ -381,15 +404,26 @@ pub fn prepare_post_processing_uniforms(
381404
render_device: Res<RenderDevice>,
382405
render_queue: Res<RenderQueue>,
383406
mut views: Query<
384-
(Entity, Option<&ChromaticAberration>, Option<&Vignette>),
385-
Or<(With<ChromaticAberration>, With<Vignette>)>,
407+
(
408+
Entity,
409+
Option<&ChromaticAberration>,
410+
Option<&Vignette>,
411+
Option<&LensDistortion>,
412+
),
413+
Or<(
414+
With<ChromaticAberration>,
415+
With<Vignette>,
416+
With<LensDistortion>,
417+
)>,
386418
>,
387419
) {
388420
post_processing_uniform_buffers.chromatic_aberration.clear();
389421
post_processing_uniform_buffers.vignette.clear();
390422

391423
// Gather up all the postprocessing settings.
392-
for (view_entity, maybe_chromatic_aberration, maybe_vignette) in views.iter_mut() {
424+
for (view_entity, maybe_chromatic_aberration, maybe_vignette, maybe_lens_distortion) in
425+
views.iter_mut()
426+
{
393427
let chromatic_aberration_uniform_buffer_offset =
394428
if let Some(chromatic_aberration) = maybe_chromatic_aberration {
395429
post_processing_uniform_buffers.chromatic_aberration.push(
@@ -425,11 +459,30 @@ pub fn prepare_post_processing_uniforms(
425459
.push(&VignetteUniform::default())
426460
};
427461

462+
let lens_distortion_uniform_buffer_offset =
463+
if let Some(lens_distortion) = maybe_lens_distortion {
464+
post_processing_uniform_buffers
465+
.lens_distortion
466+
.push(&LensDistortionUniform {
467+
intensity: lens_distortion.intensity,
468+
scale: lens_distortion.scale.max(1e-6),
469+
multiplier: lens_distortion.multiplier.max(Vec2::ZERO),
470+
center: lens_distortion.center.clamp(Vec2::ZERO, Vec2::ONE),
471+
edge_curvature: lens_distortion.edge_curvature,
472+
unused: 0,
473+
})
474+
} else {
475+
post_processing_uniform_buffers
476+
.lens_distortion
477+
.push(&LensDistortionUniform::default())
478+
};
479+
428480
commands
429481
.entity(view_entity)
430482
.insert(PostProcessingUniformBufferOffsets {
431483
chromatic_aberration: chromatic_aberration_uniform_buffer_offset,
432484
vignette: vignette_uniform_buffer_offset,
485+
lens_distortion: lens_distortion_uniform_buffer_offset,
433486
});
434487
}
435488

@@ -440,4 +493,7 @@ pub fn prepare_post_processing_uniforms(
440493
post_processing_uniform_buffers
441494
.vignette
442495
.write_buffer(&render_device, &render_queue);
496+
post_processing_uniform_buffers
497+
.lens_distortion
498+
.write_buffer(&render_device, &render_queue);
443499
}

crates/bevy_post_process/src/effect_stack/post_process.wgsl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
44
#import bevy_post_process::effect_stack::chromatic_aberration::chromatic_aberration
5+
#import bevy_post_process::effect_stack::lens_distortion::lens_distortion
56
#import bevy_post_process::effect_stack::vignette::vignette
67

78
@fragment
89
fn fragment_main(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
9-
let color = chromatic_aberration(in.uv);
10+
let distorted_uv = lens_distortion(in.uv);
11+
let color = chromatic_aberration(distorted_uv);
1012
return vec4(vignette(in.uv, color), 1.0);
1113
}

0 commit comments

Comments
 (0)