1use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
2use bevy_app::prelude::*;
3use bevy_asset::{load_internal_asset, weak_handle, Assets, Handle};
4use bevy_ecs::prelude::*;
5use bevy_image::{CompressedImageFormats, Image, ImageSampler, ImageType};
6use bevy_reflect::{std_traits::ReflectDefault, Reflect};
7use bevy_render::{
8 camera::Camera,
9 extract_component::{ExtractComponent, ExtractComponentPlugin},
10 extract_resource::{ExtractResource, ExtractResourcePlugin},
11 render_asset::{RenderAssetUsages, RenderAssets},
12 render_resource::{
13 binding_types::{sampler, texture_2d, texture_3d, uniform_buffer},
14 *,
15 },
16 renderer::RenderDevice,
17 texture::{FallbackImage, GpuImage},
18 view::{ExtractedView, ViewTarget, ViewUniform},
19 Render, RenderApp, RenderSet,
20};
21use bitflags::bitflags;
22#[cfg(not(feature = "tonemapping_luts"))]
23use tracing::error;
24
25mod node;
26
27use bevy_utils::default;
28pub use node::TonemappingNode;
29
30const TONEMAPPING_SHADER_HANDLE: Handle<Shader> =
31 weak_handle!("e239c010-c25c-42a1-b4e8-08818764d667");
32
33const TONEMAPPING_SHARED_SHADER_HANDLE: Handle<Shader> =
34 weak_handle!("61dbc544-4b30-4ca9-83bd-4751b5cfb1b1");
35
36const TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE: Handle<Shader> =
37 weak_handle!("d50e3a70-c85e-4725-a81e-72fc83281145");
38
39#[derive(Resource, Clone, ExtractResource)]
41pub struct TonemappingLuts {
42 blender_filmic: Handle<Image>,
43 agx: Handle<Image>,
44 tony_mc_mapface: Handle<Image>,
45}
46
47pub struct TonemappingPlugin;
48
49impl Plugin for TonemappingPlugin {
50 fn build(&self, app: &mut App) {
51 load_internal_asset!(
52 app,
53 TONEMAPPING_SHADER_HANDLE,
54 "tonemapping.wgsl",
55 Shader::from_wgsl
56 );
57 load_internal_asset!(
58 app,
59 TONEMAPPING_SHARED_SHADER_HANDLE,
60 "tonemapping_shared.wgsl",
61 Shader::from_wgsl
62 );
63 load_internal_asset!(
64 app,
65 TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE,
66 "lut_bindings.wgsl",
67 Shader::from_wgsl
68 );
69
70 if !app.world().is_resource_added::<TonemappingLuts>() {
71 let mut images = app.world_mut().resource_mut::<Assets<Image>>();
72
73 #[cfg(feature = "tonemapping_luts")]
74 let tonemapping_luts = {
75 TonemappingLuts {
76 blender_filmic: images.add(setup_tonemapping_lut_image(
77 include_bytes!("luts/Blender_-11_12.ktx2"),
78 ImageType::Extension("ktx2"),
79 )),
80 agx: images.add(setup_tonemapping_lut_image(
81 include_bytes!("luts/AgX-default_contrast.ktx2"),
82 ImageType::Extension("ktx2"),
83 )),
84 tony_mc_mapface: images.add(setup_tonemapping_lut_image(
85 include_bytes!("luts/tony_mc_mapface.ktx2"),
86 ImageType::Extension("ktx2"),
87 )),
88 }
89 };
90
91 #[cfg(not(feature = "tonemapping_luts"))]
92 let tonemapping_luts = {
93 let placeholder = images.add(lut_placeholder());
94 TonemappingLuts {
95 blender_filmic: placeholder.clone(),
96 agx: placeholder.clone(),
97 tony_mc_mapface: placeholder,
98 }
99 };
100
101 app.insert_resource(tonemapping_luts);
102 }
103
104 app.add_plugins(ExtractResourcePlugin::<TonemappingLuts>::default());
105
106 app.register_type::<Tonemapping>();
107 app.register_type::<DebandDither>();
108
109 app.add_plugins((
110 ExtractComponentPlugin::<Tonemapping>::default(),
111 ExtractComponentPlugin::<DebandDither>::default(),
112 ));
113
114 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
115 return;
116 };
117 render_app
118 .init_resource::<SpecializedRenderPipelines<TonemappingPipeline>>()
119 .add_systems(
120 Render,
121 prepare_view_tonemapping_pipelines.in_set(RenderSet::Prepare),
122 );
123 }
124
125 fn finish(&self, app: &mut App) {
126 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
127 return;
128 };
129 render_app.init_resource::<TonemappingPipeline>();
130 }
131}
132
133#[derive(Resource)]
134pub struct TonemappingPipeline {
135 texture_bind_group: BindGroupLayout,
136 sampler: Sampler,
137}
138
139#[derive(
141 Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
142)]
143#[extract_component_filter(With<Camera>)]
144#[reflect(Component, Debug, Hash, Default, PartialEq)]
145pub enum Tonemapping {
146 None,
148 Reinhard,
151 ReinhardLuminance,
153 AcesFitted,
159 AgX,
165 SomewhatBoringDisplayTransform,
171 #[default]
184 TonyMcMapface,
185 BlenderFilmic,
189}
190
191impl Tonemapping {
192 pub fn is_enabled(&self) -> bool {
193 *self != Tonemapping::None
194 }
195}
196
197bitflags! {
198 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
202 pub struct TonemappingPipelineKeyFlags: u8 {
203 const HUE_ROTATE = 0x01;
205 const WHITE_BALANCE = 0x02;
207 const SECTIONAL_COLOR_GRADING = 0x04;
210 }
211}
212
213#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
214pub struct TonemappingPipelineKey {
215 deband_dither: DebandDither,
216 tonemapping: Tonemapping,
217 flags: TonemappingPipelineKeyFlags,
218}
219
220impl SpecializedRenderPipeline for TonemappingPipeline {
221 type Key = TonemappingPipelineKey;
222
223 fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
224 let mut shader_defs = Vec::new();
225
226 shader_defs.push(ShaderDefVal::UInt(
227 "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
228 3,
229 ));
230 shader_defs.push(ShaderDefVal::UInt(
231 "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
232 4,
233 ));
234
235 if let DebandDither::Enabled = key.deband_dither {
236 shader_defs.push("DEBAND_DITHER".into());
237 }
238
239 if key.flags.contains(TonemappingPipelineKeyFlags::HUE_ROTATE) {
241 shader_defs.push("HUE_ROTATE".into());
242 }
243 if key
244 .flags
245 .contains(TonemappingPipelineKeyFlags::WHITE_BALANCE)
246 {
247 shader_defs.push("WHITE_BALANCE".into());
248 }
249 if key
250 .flags
251 .contains(TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING)
252 {
253 shader_defs.push("SECTIONAL_COLOR_GRADING".into());
254 }
255
256 match key.tonemapping {
257 Tonemapping::None => shader_defs.push("TONEMAP_METHOD_NONE".into()),
258 Tonemapping::Reinhard => shader_defs.push("TONEMAP_METHOD_REINHARD".into()),
259 Tonemapping::ReinhardLuminance => {
260 shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into());
261 }
262 Tonemapping::AcesFitted => shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()),
263 Tonemapping::AgX => {
264 #[cfg(not(feature = "tonemapping_luts"))]
265 error!(
266 "AgX tonemapping requires the `tonemapping_luts` feature.
267 Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
268 or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
269 );
270 shader_defs.push("TONEMAP_METHOD_AGX".into());
271 }
272 Tonemapping::SomewhatBoringDisplayTransform => {
273 shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into());
274 }
275 Tonemapping::TonyMcMapface => {
276 #[cfg(not(feature = "tonemapping_luts"))]
277 error!(
278 "TonyMcMapFace tonemapping requires the `tonemapping_luts` feature.
279 Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
280 or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
281 );
282 shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into());
283 }
284 Tonemapping::BlenderFilmic => {
285 #[cfg(not(feature = "tonemapping_luts"))]
286 error!(
287 "BlenderFilmic tonemapping requires the `tonemapping_luts` feature.
288 Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
289 or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
290 );
291 shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into());
292 }
293 }
294 RenderPipelineDescriptor {
295 label: Some("tonemapping pipeline".into()),
296 layout: vec![self.texture_bind_group.clone()],
297 vertex: fullscreen_shader_vertex_state(),
298 fragment: Some(FragmentState {
299 shader: TONEMAPPING_SHADER_HANDLE,
300 shader_defs,
301 entry_point: "fragment".into(),
302 targets: vec![Some(ColorTargetState {
303 format: ViewTarget::TEXTURE_FORMAT_HDR,
304 blend: None,
305 write_mask: ColorWrites::ALL,
306 })],
307 }),
308 primitive: PrimitiveState::default(),
309 depth_stencil: None,
310 multisample: MultisampleState::default(),
311 push_constant_ranges: Vec::new(),
312 zero_initialize_workgroup_memory: false,
313 }
314 }
315}
316
317impl FromWorld for TonemappingPipeline {
318 fn from_world(render_world: &mut World) -> Self {
319 let mut entries = DynamicBindGroupLayoutEntries::new_with_indices(
320 ShaderStages::FRAGMENT,
321 (
322 (0, uniform_buffer::<ViewUniform>(true)),
323 (
324 1,
325 texture_2d(TextureSampleType::Float { filterable: false }),
326 ),
327 (2, sampler(SamplerBindingType::NonFiltering)),
328 ),
329 );
330 let lut_layout_entries = get_lut_bind_group_layout_entries();
331 entries =
332 entries.extend_with_indices(((3, lut_layout_entries[0]), (4, lut_layout_entries[1])));
333
334 let render_device = render_world.resource::<RenderDevice>();
335 let tonemap_texture_bind_group = render_device
336 .create_bind_group_layout("tonemapping_hdr_texture_bind_group_layout", &entries);
337
338 let sampler = render_device.create_sampler(&SamplerDescriptor::default());
339
340 TonemappingPipeline {
341 texture_bind_group: tonemap_texture_bind_group,
342 sampler,
343 }
344 }
345}
346
347#[derive(Component)]
348pub struct ViewTonemappingPipeline(CachedRenderPipelineId);
349
350pub fn prepare_view_tonemapping_pipelines(
351 mut commands: Commands,
352 pipeline_cache: Res<PipelineCache>,
353 mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
354 upscaling_pipeline: Res<TonemappingPipeline>,
355 view_targets: Query<
356 (
357 Entity,
358 &ExtractedView,
359 Option<&Tonemapping>,
360 Option<&DebandDither>,
361 ),
362 With<ViewTarget>,
363 >,
364) {
365 for (entity, view, tonemapping, dither) in view_targets.iter() {
366 let mut flags = TonemappingPipelineKeyFlags::empty();
368 flags.set(
369 TonemappingPipelineKeyFlags::HUE_ROTATE,
370 view.color_grading.global.hue != 0.0,
371 );
372 flags.set(
373 TonemappingPipelineKeyFlags::WHITE_BALANCE,
374 view.color_grading.global.temperature != 0.0 || view.color_grading.global.tint != 0.0,
375 );
376 flags.set(
377 TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING,
378 view.color_grading
379 .all_sections()
380 .any(|section| *section != default()),
381 );
382
383 let key = TonemappingPipelineKey {
384 deband_dither: *dither.unwrap_or(&DebandDither::Disabled),
385 tonemapping: *tonemapping.unwrap_or(&Tonemapping::None),
386 flags,
387 };
388 let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key);
389
390 commands
391 .entity(entity)
392 .insert(ViewTonemappingPipeline(pipeline));
393 }
394}
395#[derive(
397 Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
398)]
399#[extract_component_filter(With<Camera>)]
400#[reflect(Component, Debug, Hash, Default, PartialEq)]
401pub enum DebandDither {
402 #[default]
403 Disabled,
404 Enabled,
405}
406
407pub fn get_lut_bindings<'a>(
408 images: &'a RenderAssets<GpuImage>,
409 tonemapping_luts: &'a TonemappingLuts,
410 tonemapping: &Tonemapping,
411 fallback_image: &'a FallbackImage,
412) -> (&'a TextureView, &'a Sampler) {
413 let image = match tonemapping {
414 Tonemapping::None
416 | Tonemapping::Reinhard
417 | Tonemapping::ReinhardLuminance
418 | Tonemapping::AcesFitted
419 | Tonemapping::AgX
420 | Tonemapping::SomewhatBoringDisplayTransform => &tonemapping_luts.agx,
421 Tonemapping::TonyMcMapface => &tonemapping_luts.tony_mc_mapface,
422 Tonemapping::BlenderFilmic => &tonemapping_luts.blender_filmic,
423 };
424 let lut_image = images.get(image).unwrap_or(&fallback_image.d3);
425 (&lut_image.texture_view, &lut_image.sampler)
426}
427
428pub fn get_lut_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 2] {
429 [
430 texture_3d(TextureSampleType::Float { filterable: true }),
431 sampler(SamplerBindingType::Filtering),
432 ]
433}
434
435#[expect(clippy::allow_attributes, reason = "`dead_code` is not always linted.")]
436#[allow(
437 dead_code,
438 reason = "There is unused code when the `tonemapping_luts` feature is disabled."
439)]
440fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image {
441 let image_sampler = ImageSampler::Descriptor(bevy_image::ImageSamplerDescriptor {
442 label: Some("Tonemapping LUT sampler".to_string()),
443 address_mode_u: bevy_image::ImageAddressMode::ClampToEdge,
444 address_mode_v: bevy_image::ImageAddressMode::ClampToEdge,
445 address_mode_w: bevy_image::ImageAddressMode::ClampToEdge,
446 mag_filter: bevy_image::ImageFilterMode::Linear,
447 min_filter: bevy_image::ImageFilterMode::Linear,
448 mipmap_filter: bevy_image::ImageFilterMode::Linear,
449 ..default()
450 });
451 Image::from_buffer(
452 #[cfg(all(debug_assertions, feature = "dds"))]
453 "Tonemapping LUT sampler".to_string(),
454 bytes,
455 image_type,
456 CompressedImageFormats::NONE,
457 false,
458 image_sampler,
459 RenderAssetUsages::RENDER_WORLD,
460 )
461 .unwrap()
462}
463
464pub fn lut_placeholder() -> Image {
465 let format = TextureFormat::Rgba8Unorm;
466 let data = vec![255, 0, 255, 255];
467 Image {
468 data: Some(data),
469 texture_descriptor: TextureDescriptor {
470 size: Extent3d {
471 width: 1,
472 height: 1,
473 depth_or_array_layers: 1,
474 },
475 format,
476 dimension: TextureDimension::D3,
477 label: None,
478 mip_level_count: 1,
479 sample_count: 1,
480 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
481 view_formats: &[],
482 },
483 sampler: ImageSampler::Default,
484 texture_view_descriptor: None,
485 asset_usage: RenderAssetUsages::RENDER_WORLD,
486 }
487}