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