bevy_post_process/auto_exposure/
mod.rs1use bevy_app::prelude::*;
2use bevy_asset::{embedded_asset, AssetApp, Assets, Handle};
3use bevy_ecs::prelude::*;
4use bevy_render::{
5 diagnostic::RecordDiagnostics,
6 extract_component::ExtractComponentPlugin,
7 globals::GlobalsBuffer,
8 render_asset::{RenderAssetPlugin, RenderAssets},
9 render_resource::{
10 BindGroupEntries, Buffer, BufferBinding, BufferDescriptor, BufferUsages,
11 ComputePassDescriptor, PipelineCache, ShaderType, SpecializedComputePipelines,
12 },
13 renderer::{RenderContext, RenderDevice, ViewQuery},
14 texture::{FallbackImage, GpuImage},
15 view::{ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms},
16 ExtractSchedule, GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems,
17};
18
19mod buffers;
20mod compensation_curve;
21mod pipeline;
22mod settings;
23
24use buffers::{extract_buffers, prepare_buffers, AutoExposureBuffers};
25pub use compensation_curve::{AutoExposureCompensationCurve, AutoExposureCompensationCurveError};
26use pipeline::{AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline};
27pub use settings::AutoExposure;
28
29use crate::auto_exposure::{
30 compensation_curve::GpuAutoExposureCompensationCurve, pipeline::init_auto_exposure_pipeline,
31};
32use bevy_core_pipeline::{
33 schedule::{Core3d, Core3dSystems},
34 tonemapping::tonemapping,
35};
36
37pub struct AutoExposurePlugin;
41
42#[derive(impl bevy_ecs::resource::Resource for AutoExposureResources where
Self: ::core::marker::Send + ::core::marker::Sync + 'static {}Resource)]
43struct AutoExposureResources {
44 histogram: Buffer,
45}
46
47impl Plugin for AutoExposurePlugin {
48 fn build(&self, app: &mut App) {
49 {
{
let mut embedded =
app.world_mut().resource_mut::<::bevy_asset::io::embedded::EmbeddedAssetRegistry>();
let path =
{
let crate_name =
"bevy_post_process::auto_exposure".split(':').next().unwrap();
::bevy_asset::io::embedded::_embedded_asset_path(crate_name,
"src".as_ref(), "src/auto_exposure/mod.rs".as_ref(),
"auto_exposure.wgsl".as_ref())
};
let watched_path =
::bevy_asset::io::embedded::watched_path("src/auto_exposure/mod.rs",
"auto_exposure.wgsl");
embedded.insert_asset(watched_path, &path,
b"// Auto exposure\n//\n// This shader computes an auto exposure value for the current frame,\n// which is then used as an exposure correction in the tone mapping shader.\n//\n// The auto exposure value is computed in two passes:\n// * The compute_histogram pass calculates a histogram of the luminance values in the scene,\n// taking into account the metering mask texture. The metering mask is a grayscale texture\n// that defines the areas of the screen that should be given more weight when calculating\n// the average luminance value. For example, the middle area of the screen might be more important\n// than the edges.\n// * The compute_average pass calculates the average luminance value of the scene, taking\n// into account the low_percent and high_percent settings. These settings define the\n// percentage of the histogram that should be excluded when calculating the average. This\n// is useful to avoid overexposure when you have a lot of shadows, or underexposure when you\n// have a lot of bright specular reflections.\n//\n// The final target_exposure is finally used to smoothly adjust the exposure value over time.\n\n#import bevy_render::view::View\n#import bevy_render::globals::Globals\n\n// Constant to convert RGB to luminance, taken from Real Time Rendering, Vol 4 pg. 278, 4th edition\nconst RGB_TO_LUM = vec3<f32>(0.2125, 0.7154, 0.0721);\n\nstruct AutoExposure {\n min_log_lum: f32,\n inv_log_lum_range: f32,\n log_lum_range: f32,\n low_percent: f32,\n high_percent: f32,\n speed_up: f32,\n speed_down: f32,\n exponential_transition_distance: f32,\n}\n\nstruct CompensationCurve {\n min_log_lum: f32,\n inv_log_lum_range: f32,\n min_compensation: f32,\n compensation_range: f32,\n}\n\n@group(0) @binding(0) var<uniform> globals: Globals;\n\n@group(0) @binding(1) var<uniform> settings: AutoExposure;\n\n@group(0) @binding(2) var tex_color: texture_2d<f32>;\n\n@group(0) @binding(3) var tex_mask: texture_2d<f32>;\n\n@group(0) @binding(4) var tex_compensation: texture_1d<f32>;\n\n@group(0) @binding(5) var<uniform> compensation_curve: CompensationCurve;\n\n@group(0) @binding(6) var<storage, read_write> histogram: array<atomic<u32>, 64>;\n\n@group(0) @binding(7) var<storage, read_write> exposure: f32;\n\n@group(0) @binding(8) var<storage, read_write> view: View;\n\nvar<workgroup> histogram_shared: array<atomic<u32>, 64>;\n\n// For a given color, return the histogram bin index\nfn color_to_bin(hdr: vec3<f32>) -> u32 {\n // Convert color to luminance\n let lum = dot(hdr, RGB_TO_LUM);\n\n if lum < exp2(settings.min_log_lum) {\n return 0u;\n }\n\n // Calculate the log_2 luminance and express it as a value in [0.0, 1.0]\n // where 0.0 represents the minimum luminance, and 1.0 represents the max.\n let log_lum = saturate((log2(lum) - settings.min_log_lum) * settings.inv_log_lum_range);\n\n // Map [0, 1] to [1, 63]. The zeroth bin is handled by the epsilon check above.\n return u32(log_lum * 62.0 + 1.0);\n}\n\n// Read the metering mask at the given UV coordinates, returning a weight for the histogram.\n//\n// Since the histogram is summed in the compute_average step, there is a limit to the amount of\n// distinct values that can be represented. When using the chosen value of 16, the maximum\n// amount of pixels that can be weighted and summed is 2^32 / 16 = 16384^2.\nfn metering_weight(coords: vec2<f32>) -> u32 {\n let pos = vec2<i32>(coords * vec2<f32>(textureDimensions(tex_mask)));\n let mask = textureLoad(tex_mask, pos, 0).r;\n return u32(mask * 16.0);\n}\n\n@compute @workgroup_size(16, 16, 1)\nfn compute_histogram(\n @builtin(global_invocation_id) global_invocation_id: vec3<u32>,\n @builtin(local_invocation_index) local_invocation_index: u32\n) {\n // Clear the workgroup shared histogram\n if local_invocation_index < 64 {\n histogram_shared[local_invocation_index] = 0u;\n }\n\n // Wait for all workgroup threads to clear the shared histogram\n workgroupBarrier();\n\n let dim = vec2<u32>(textureDimensions(tex_color));\n let uv = vec2<f32>(global_invocation_id.xy) / vec2<f32>(dim);\n\n if global_invocation_id.x < dim.x && global_invocation_id.y < dim.y {\n let col = textureLoad(tex_color, vec2<i32>(global_invocation_id.xy), 0).rgb;\n let index = color_to_bin(col);\n let weight = metering_weight(uv);\n\n // Increment the shared histogram bin by the weight obtained from the metering mask\n atomicAdd(&histogram_shared[index], weight);\n }\n\n // Wait for all workgroup threads to finish updating the workgroup histogram\n workgroupBarrier();\n\n // Accumulate the workgroup histogram into the global histogram.\n // Note that the global histogram was not cleared at the beginning,\n // as it will be cleared in compute_average.\n atomicAdd(&histogram[local_invocation_index], histogram_shared[local_invocation_index]);\n}\n\n@compute @workgroup_size(1, 1, 1)\nfn compute_average(@builtin(local_invocation_index) local_index: u32) {\n var histogram_sum = 0u;\n\n // Calculate the cumulative histogram and clear the histogram bins.\n // Each bin in the cumulative histogram contains the sum of all bins up to that point.\n // This way we can quickly exclude the portion of lowest and highest samples as required by\n // the low_percent and high_percent settings.\n for (var i=0u; i<64u; i+=1u) {\n histogram_sum += histogram[i];\n histogram_shared[i] = histogram_sum;\n\n // Clear the histogram bin for the next frame\n histogram[i] = 0u;\n }\n\n let first_index = u32(f32(histogram_sum) * settings.low_percent);\n let last_index = u32(f32(histogram_sum) * settings.high_percent);\n\n var count = 0u;\n var sum = 0.0;\n for (var i=1u; i<64u; i+=1u) {\n // The number of pixels in the bin. The histogram values are clamped to\n // first_index and last_index to exclude the lowest and highest samples.\n let bin_count =\n clamp(histogram_shared[i], first_index, last_index) -\n clamp(histogram_shared[i - 1u], first_index, last_index);\n\n sum += f32(bin_count) * f32(i);\n count += bin_count;\n }\n\n var avg_lum = settings.min_log_lum;\n\n if count > 0u {\n // The average luminance of the included histogram samples.\n avg_lum = sum / (f32(count) * 63.0)\n * settings.log_lum_range\n + settings.min_log_lum;\n }\n\n // The position in the compensation curve texture to sample for avg_lum.\n let u = (avg_lum - compensation_curve.min_log_lum) * compensation_curve.inv_log_lum_range;\n\n // The target exposure is the negative of the average log luminance.\n // The compensation value is added to the target exposure to adjust the exposure for\n // artistic purposes.\n let target_exposure = textureLoad(tex_compensation, i32(saturate(u) * 255.0), 0).r\n * compensation_curve.compensation_range\n + compensation_curve.min_compensation\n - avg_lum;\n\n // Smoothly adjust the `exposure` towards the `target_exposure`\n let delta = target_exposure - exposure;\n if target_exposure > exposure {\n let speed_down = settings.speed_down * globals.delta_time;\n let exp_down = speed_down / settings.exponential_transition_distance;\n exposure = exposure + min(speed_down, delta * exp_down);\n } else {\n let speed_up = settings.speed_up * globals.delta_time;\n let exp_up = speed_up / settings.exponential_transition_distance;\n exposure = exposure + max(-speed_up, delta * exp_up);\n }\n\n // Apply the exposure to the color grading settings, from where it will be used for the color\n // grading pass.\n view.color_grading.exposure += exposure;\n}\n");
}
};embedded_asset!(app, "auto_exposure.wgsl");
50
51 app.add_plugins(RenderAssetPlugin::<GpuAutoExposureCompensationCurve>::default())
52 .init_asset::<AutoExposureCompensationCurve>()
53 .register_asset_reflect::<AutoExposureCompensationCurve>();
54 app.world_mut()
55 .resource_mut::<Assets<AutoExposureCompensationCurve>>()
56 .insert(&Handle::default(), AutoExposureCompensationCurve::default())
57 .unwrap();
58
59 app.add_plugins(ExtractComponentPlugin::<AutoExposure>::default());
60
61 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
62 return;
63 };
64
65 render_app
66 .init_gpu_resource::<SpecializedComputePipelines<AutoExposurePipeline>>()
67 .init_resource::<AutoExposureBuffers>()
68 .add_systems(
69 RenderStartup,
70 (init_auto_exposure_pipeline, init_auto_exposure_resources),
71 )
72 .add_systems(ExtractSchedule, extract_buffers)
73 .add_systems(
74 Render,
75 (
76 prepare_buffers.in_set(RenderSystems::Prepare),
77 queue_view_auto_exposure_pipelines.in_set(RenderSystems::Queue),
78 ),
79 )
80 .add_systems(
81 Core3d,
82 auto_exposure
83 .before(tonemapping)
84 .in_set(Core3dSystems::PostProcess),
85 );
86 }
87}
88
89pub fn init_auto_exposure_resources(mut commands: Commands, render_device: Res<RenderDevice>) {
90 commands.insert_resource(AutoExposureResources {
91 histogram: render_device.create_buffer(&BufferDescriptor {
92 label: Some("histogram buffer"),
93 size: pipeline::HISTOGRAM_BIN_COUNT * 4,
94 usage: BufferUsages::STORAGE,
95 mapped_at_creation: false,
96 }),
97 });
98}
99
100fn queue_view_auto_exposure_pipelines(
101 mut commands: Commands,
102 pipeline_cache: Res<PipelineCache>,
103 mut compute_pipelines: ResMut<SpecializedComputePipelines<AutoExposurePipeline>>,
104 pipeline: Res<AutoExposurePipeline>,
105 view_targets: Query<(Entity, &AutoExposure)>,
106) {
107 for (entity, auto_exposure) in view_targets.iter() {
108 let histogram_pipeline =
109 compute_pipelines.specialize(&pipeline_cache, &pipeline, AutoExposurePass::Histogram);
110 let average_pipeline =
111 compute_pipelines.specialize(&pipeline_cache, &pipeline, AutoExposurePass::Average);
112
113 commands.entity(entity).insert(ViewAutoExposurePipeline {
114 histogram_pipeline,
115 mean_luminance_pipeline: average_pipeline,
116 compensation_curve: auto_exposure.compensation_curve.clone(),
117 metering_mask: auto_exposure.metering_mask.clone(),
118 });
119 }
120}
121
122fn auto_exposure(
123 view: ViewQuery<(
124 &ViewUniformOffset,
125 &ViewTarget,
126 &ViewAutoExposurePipeline,
127 &ExtractedView,
128 )>,
129 pipeline_cache: Res<PipelineCache>,
130 pipeline: Res<AutoExposurePipeline>,
131 resources: Res<AutoExposureResources>,
132 view_uniforms: Res<ViewUniforms>,
133 globals_buffer: Res<GlobalsBuffer>,
134 auto_exposure_buffers: Res<AutoExposureBuffers>,
135 fallback: Res<FallbackImage>,
136 gpu_images: Res<RenderAssets<GpuImage>>,
137 compensation_curves: Res<RenderAssets<GpuAutoExposureCompensationCurve>>,
138 mut ctx: RenderContext,
139) {
140 let view_entity = view.entity();
141 let (view_uniform_offset, view_target, auto_exposure_pipeline, extracted_view) =
142 view.into_inner();
143
144 let Some(auto_exposure_buffer) = auto_exposure_buffers.buffers.get(&view_entity) else {
145 return;
146 };
147
148 let (Some(histogram_pipeline), Some(average_pipeline)) = (
149 pipeline_cache.get_compute_pipeline(auto_exposure_pipeline.histogram_pipeline),
150 pipeline_cache.get_compute_pipeline(auto_exposure_pipeline.mean_luminance_pipeline),
151 ) else {
152 return;
153 };
154
155 let view_uniforms_buffer = view_uniforms.uniforms.buffer().unwrap();
156 let source = view_target.main_texture_view();
157
158 let mask = gpu_images
159 .get(&auto_exposure_pipeline.metering_mask)
160 .map(|i| &i.texture_view)
161 .unwrap_or(&fallback.d2.texture_view);
162
163 let Some(compensation_curve) =
164 compensation_curves.get(&auto_exposure_pipeline.compensation_curve)
165 else {
166 return;
167 };
168
169 let compute_bind_group = ctx.render_device().create_bind_group(
170 None,
171 &pipeline_cache.get_bind_group_layout(&pipeline.histogram_layout),
172 &BindGroupEntries::sequential((
173 &globals_buffer.buffer,
174 &auto_exposure_buffer.settings,
175 source,
176 mask,
177 &compensation_curve.texture_view,
178 &compensation_curve.extents,
179 resources.histogram.as_entire_buffer_binding(),
180 &auto_exposure_buffer.state,
181 BufferBinding {
182 buffer: view_uniforms_buffer,
183 size: Some(ViewUniform::min_size()),
184 offset: 0,
185 },
186 )),
187 );
188
189 let diagnostics = ctx.diagnostic_recorder();
190 let diagnostics = diagnostics.as_deref();
191 let time_span = diagnostics.time_span(ctx.command_encoder(), "auto_exposure");
192
193 {
194 let mut compute_pass = ctx
195 .command_encoder()
196 .begin_compute_pass(&ComputePassDescriptor {
197 label: Some("auto_exposure"),
198 timestamp_writes: None,
199 });
200
201 compute_pass.set_bind_group(0, &compute_bind_group, &[view_uniform_offset.offset]);
202 compute_pass.set_pipeline(histogram_pipeline);
203 compute_pass.dispatch_workgroups(
204 extracted_view.viewport.z.div_ceil(16),
205 extracted_view.viewport.w.div_ceil(16),
206 1,
207 );
208 compute_pass.set_pipeline(average_pipeline);
209 compute_pass.dispatch_workgroups(1, 1, 1);
210 }
211
212 time_span.end(ctx.command_encoder());
213}