Search across packages, versions, files, declarations, and comments. Use multiple terms to narrow results (AND), and add @vertex, @fragment, or @compute to filter by shader stage.
1#define_import_path bevy_core_pipeline::tonemapping
3#import bevy_render::{
4 view::ColorGrading,
5 color_operations::{hsv_to_rgb, rgb_to_hsv},
6 maths::{PI_2, powsafe},
7}
9#import bevy_core_pipeline::tonemapping_lut_bindings::{
10 dt_lut_texture,
11 dt_lut_sampler,
14// Half the size of the crossfade region between shadows and midtones and
15// between midtones and highlights. This value, 0.1, corresponds to 10% of the
16// gamut on either side of the cutoff point.
17const LEVEL_MARGIN: f32 = 0.1;
19// The inverse reciprocal of twice the above, used when scaling the midtone
20// region.
21const LEVEL_MARGIN_DIV: f32 = 0.5 / LEVEL_MARGIN;
23fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {
24 // Don't include code that will try to sample from LUTs if tonemap method doesn't require it
25 // Allows this file to be imported without necessarily needing the lut texture bindings
26#ifdef TONEMAP_METHOD_AGX
27 return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
28#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE
29 return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
30#else ifdef TONEMAP_METHOD_BLENDER_FILMIC
31 return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
32#else
33 return vec3(1.0, 0.0, 1.0);
34 #endif
37// --------------------------------------
38// --- SomewhatBoringDisplayTransform ---
39// --------------------------------------
40// By Tomasz Stachowiak
42fn rgb_to_ycbcr(col: vec3<f32>) -> vec3<f32> {
43 let m = mat3x3<f32>(
44 0.2126, 0.7152, 0.0722,
45 -0.1146, -0.3854, 0.5,
46 0.5, -0.4542, -0.0458
47 );
48 return col * m;
51fn tonemap_curve(v: f32) -> f32 {
52#ifdef 0
53 // Large linear part in the lows, but compresses highs.
54 float c = v + v * v + 0.5 * v * v * v;
55 return c / (1.0 + c);
56#else
57 return 1.0 - exp(-v);
58#endif
61fn tonemap_curve3_(v: vec3<f32>) -> vec3<f32> {
62 return vec3(tonemap_curve(v.r), tonemap_curve(v.g), tonemap_curve(v.b));
65fn somewhat_boring_display_transform(col: vec3<f32>) -> vec3<f32> {
66 var boring_color = col;
67 let ycbcr = rgb_to_ycbcr(boring_color);
69 let bt = tonemap_curve(length(ycbcr.yz) * 2.4);
70 var desat = max((bt - 0.7) * 0.8, 0.0);
71 desat *= desat;
73 let desat_col = mix(boring_color.rgb, ycbcr.xxx, desat);
75 let tm_luma = tonemap_curve(ycbcr.x);
76 let tm0 = boring_color.rgb * max(0.0, tm_luma / max(1e-5, tonemapping_luminance(boring_color.rgb)));
77 let final_mult = 0.97;
78 let tm1 = tonemap_curve3_(desat_col);
80 boring_color = mix(tm0, tm1, bt * bt);
82 return boring_color * final_mult;
85// ------------------------------------------
86// ------------- Tony McMapface -------------
87// ------------------------------------------
88// By Tomasz Stachowiak
89// https://github.com/h3r2tic/tony-mc-mapface
91const TONY_MC_MAPFACE_LUT_DIMS: f32 = 48.0;
93fn sample_tony_mc_mapface_lut(stimulus: vec3<f32>) -> vec3<f32> {
94 var uv = (stimulus / (stimulus + 1.0)) * (f32(TONY_MC_MAPFACE_LUT_DIMS - 1.0) / f32(TONY_MC_MAPFACE_LUT_DIMS)) + 0.5 / f32(TONY_MC_MAPFACE_LUT_DIMS);
95 return sample_current_lut(saturate(uv)).rgb;
98// ---------------------------------
99// ---------- ACES Fitted ----------
100// ---------------------------------
102// Same base implementation that Godot 4.0 uses for Tonemap ACES.
104// https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
106// The code in this file was originally written by Stephen Hill (@self_shadow), who deserves all
107// credit for coming up with this fit and implementing it. Buy him a beer next time you see him. :)
109fn RRTAndODTFit(v: vec3<f32>) -> vec3<f32> {
110 let a = v * (v + 0.0245786) - 0.000090537;
111 let b = v * (0.983729 * v + 0.4329510) + 0.238081;
112 return a / b;
115fn ACESFitted(color: vec3<f32>) -> vec3<f32> {
116 var fitted_color = color;
118 // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
119 let rgb_to_rrt = mat3x3<f32>(
120 vec3(0.59719, 0.35458, 0.04823),
121 vec3(0.07600, 0.90834, 0.01566),
122 vec3(0.02840, 0.13383, 0.83777)
123 );
125 // ODT_SAT => XYZ => D60_2_D65 => sRGB
126 let odt_to_rgb = mat3x3<f32>(
127 vec3(1.60475, -0.53108, -0.07367),
128 vec3(-0.10208, 1.10813, -0.00605),
129 vec3(-0.00327, -0.07276, 1.07602)
130 );
132 fitted_color *= rgb_to_rrt;
134 // Apply RRT and ODT
135 fitted_color = RRTAndODTFit(fitted_color);
137 fitted_color *= odt_to_rgb;
139 // Clamp to [0, 1]
140 fitted_color = saturate(fitted_color);
142 return fitted_color;
145// -------------------------------
146// ------------- AgX -------------
147// -------------------------------
148// By Troy Sobotka
149// https://github.com/MrLixm/AgXc
150// https://github.com/sobotka/AgX
152/*
153 Increase color saturation of the given color data.
154 :param color: expected sRGB primaries input
155 :param saturationAmount: expected 0-1 range with 1=neutral, 0=no saturation.
156 -- ref[2] [4]
157*/
158fn saturation(color: vec3<f32>, saturationAmount: f32) -> vec3<f32> {
159 let luma = tonemapping_luminance(color);
160 return mix(vec3(luma), color, vec3(saturationAmount));
163/*
164 Output log domain encoded data.
165 Similar to OCIO lg2 AllocationTransform.
166 ref[0]
167*/
168fn convertOpenDomainToNormalizedLog2_(color: vec3<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> {
169 let in_midgray = 0.18;
171 // remove negative before log transform
172 var normalized_color = max(vec3(0.0), color);
173 // avoid infinite issue with log -- ref[1]
174 normalized_color = select(normalized_color, 0.00001525878 + normalized_color, normalized_color < vec3<f32>(0.00003051757));
175 normalized_color = clamp(
176 log2(normalized_color / in_midgray),
177 vec3(minimum_ev),
178 vec3(maximum_ev)
179 );
180 let total_exposure = maximum_ev - minimum_ev;
182 return (normalized_color - minimum_ev) / total_exposure;
185// Inverse of above
186fn convertNormalizedLog2ToOpenDomain(color: vec3<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> {
187 var open_color = color;
188 let in_midgray = 0.18;
189 let total_exposure = maximum_ev - minimum_ev;
191 open_color = (open_color * total_exposure) + minimum_ev;
192 open_color = pow(vec3(2.0), open_color);
193 open_color = open_color * in_midgray;
195 return open_color;
199/*=================
200 Main processes
201=================*/
203// Prepare the data for display encoding. Converted to log domain.
204fn applyAgXLog(Image: vec3<f32>) -> vec3<f32> {
205 var prepared_image = max(vec3(0.0), Image); // clamp negatives
206 let r = dot(prepared_image, vec3(0.84247906, 0.0784336, 0.07922375));
207 let g = dot(prepared_image, vec3(0.04232824, 0.87846864, 0.07916613));
208 let b = dot(prepared_image, vec3(0.04237565, 0.0784336, 0.87914297));
209 prepared_image = vec3(r, g, b);
211 prepared_image = convertOpenDomainToNormalizedLog2_(prepared_image, -10.0, 6.5);
213 prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0));
214 return prepared_image;
217fn applyLUT3D(Image: vec3<f32>, block_size: f32) -> vec3<f32> {
218 return sample_current_lut(Image * ((block_size - 1.0) / block_size) + 0.5 / block_size).rgb;
221// -------------------------
222// -------------------------
223// -------------------------
225fn sample_blender_filmic_lut(stimulus: vec3<f32>) -> vec3<f32> {
226 let block_size = 64.0;
227 let normalized = saturate(convertOpenDomainToNormalizedLog2_(stimulus, -11.0, 12.0));
228 return applyLUT3D(normalized, block_size);
231// from https://64.github.io/tonemapping/
232// reinhard on RGB oversaturates colors
233fn tonemapping_reinhard(color: vec3<f32>) -> vec3<f32> {
234 return color / (1.0 + color);
237// luminance coefficients from Rec. 709.
238// https://en.wikipedia.org/wiki/Rec._709
239fn tonemapping_luminance(v: vec3<f32>) -> f32 {
240 return dot(v, vec3<f32>(0.2126, 0.7152, 0.0722));
243fn tonemapping_change_luminance(c_in: vec3<f32>, l_out: f32) -> vec3<f32> {
244 let l_in = tonemapping_luminance(c_in);
245 return c_in * (l_out / l_in);
248fn tonemapping_reinhard_luminance(color: vec3<f32>) -> vec3<f32> {
249 let l_old = tonemapping_luminance(color);
250 let l_new = l_old / (1.0 + l_old);
251 return tonemapping_change_luminance(color, l_new);
254fn rgb_to_srgb_simple(color: vec3<f32>) -> vec3<f32> {
255 return pow(color, vec3<f32>(1.0 / 2.2));
258// PBR Neutral tone mapping
259// https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/pbrNeutral.glsl
260// Adapted under Apache 2.0, per https://github.com/KhronosGroup/ToneMapping/tree/main/LICENSES
261fn tonemapping_pbr_neutral(color_in: vec3<f32>) -> vec3<f32> {
262 // Parameter controlling when highlight compression starts
263 // (`K_s` in specification)
264 const start_compression: f32 = 0.8 - 0.04;
266 // Parameter controlling the speed of desaturation
267 // (`K-d` in specification)
268 const desaturation: f32 = 0.15;
270 // `x` in the specification equation
271 let min_channel = min(color_in.r, min(color_in.g, color_in.b));
273 // The amount that the "toe" adjustment reduces the color
274 // `f` in the specification equation
275 let offset = select(0.04, min_channel - 6.25 * min_channel * min_channel, min_channel < 0.08);
277 // The original color, minus the "toe" reduction
278 // `c_in - f` in the specification equation
279 let offset_color = color_in - offset;
281 // Maximum of all offset color channels
282 // `p` in the specification equation
283 let max_channel = max(offset_color.r, max(offset_color.g, offset_color.b));
284 if max_channel < start_compression {
285 // "toe" at the low-end; or uncompressed, offset color for most of the range
286 return offset_color;
287 }
289 // This doesn't exist in the specification equation. It is part of optimizing
290 // the math for computation.
291 let d = 1.0 - start_compression;
293 // Maximum color channel, scaled to asymptotically approach 1.0
294 let new_max_channel = 1.0 - d * d / (max_channel + d - start_compression);
296 // Full color, from offset color, with the same scale applied as `new_max_channel`
297 // `p_n` in the specification equation
298 let color = offset_color * (new_max_channel / max_channel);
300 // Amount to desaturate, used as the blend factor when mixing between
301 // full color and desaturated.
302 let g = 1.0 - 1.0 / (desaturation * (max_channel - new_max_channel) + 1.0);
303 return mix(color, vec3(new_max_channel), g);
306// Source: Advanced VR Rendering, GDC 2015, Alex Vlachos, Valve, Slide 49
307// https://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf
308fn screen_space_dither(frag_coord: vec2<f32>) -> vec3<f32> {
309 var dither = vec3<f32>(dot(vec2<f32>(171.0, 231.0), frag_coord)).xxx;
310 dither = fract(dither.rgb / vec3<f32>(103.0, 71.0, 97.0));
311 return (dither - 0.5) / 255.0;
314// Performs the "sectional" color grading: i.e. the color grading that applies
315// individually to shadows, midtones, and highlights.
316fn sectional_color_grading(
317 in: vec3<f32>,
318 color_grading: ptr<function, ColorGrading>,
319) -> vec3<f32> {
320 var color = in;
322 // Determine whether the color is a shadow, midtone, or highlight. Colors
323 // close to the edges are considered a mix of both, to avoid sharp
324 // discontinuities. The formulas are taken from Blender's compositor.
326 let level = (color.r + color.g + color.b) / 3.0;
328 // Determine whether this color is a shadow, midtone, or highlight. If close
329 // to the cutoff points, blend between the two to avoid sharp color
330 // discontinuities.
331 var levels = vec3(0.0);
332 let midtone_range = (*color_grading).midtone_range;
333 if (level < midtone_range.x - LEVEL_MARGIN) {
334 levels.x = 1.0;
335 } else if (level < midtone_range.x + LEVEL_MARGIN) {
336 levels.y = ((level - midtone_range.x) * LEVEL_MARGIN_DIV) + 0.5;
337 levels.z = 1.0 - levels.y;
338 } else if (level < midtone_range.y - LEVEL_MARGIN) {
339 levels.y = 1.0;
340 } else if (level < midtone_range.y + LEVEL_MARGIN) {
341 levels.z = ((level - midtone_range.y) * LEVEL_MARGIN_DIV) + 0.5;
342 levels.y = 1.0 - levels.z;
343 } else {
344 levels.z = 1.0;
345 }
347 // Calculate contrast/saturation/gamma/gain/lift.
348 let contrast = dot(levels, (*color_grading).contrast);
349 let saturation = dot(levels, (*color_grading).saturation);
350 let gamma = dot(levels, (*color_grading).gamma);
351 let gain = dot(levels, (*color_grading).gain);
352 let lift = dot(levels, (*color_grading).lift);
354 // Adjust saturation and contrast.
355 let luma = tonemapping_luminance(color);
356 color = luma + saturation * (color - luma);
357 color = 0.5 + (color - 0.5) * contrast;
359 // The [ASC CDL] formula for color correction. Given *i*, an input color, we
360 // have:
361 //
362 // out = (i × s + o)ⁿ
363 //
364 // Following the normal photographic naming convention, *gain* is the *s*
365 // factor, *lift* is the *o* term, and the inverse of *gamma* is the *n*
366 // exponent.
367 //
368 // [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
369 color = powsafe(color * gain + lift, 1.0 / gamma);
371 // Account for exposure.
372 color = color * powsafe(vec3(2.0), (*color_grading).exposure);
373 return max(color, vec3(0.0));
376fn tone_mapping(in: vec4<f32>, in_color_grading: ColorGrading) -> vec4<f32> {
377 var color = max(in.rgb, vec3(0.0));
378 var color_grading = in_color_grading; // So we can take pointers to it.
380 // Rotate hue if needed, by converting to and from HSV. Remember that hue is
381 // an angle, so it needs to be modulo 2π.
382#ifdef HUE_ROTATE
383 var hsv = rgb_to_hsv(color);
384 hsv.r = (hsv.r + color_grading.hue) % PI_2;
385 color = hsv_to_rgb(hsv);
386#endif
388 // Perform white balance correction. Conveniently, this is a linear
389 // transform. The matrix was pre-calculated from the temperature and tint
390 // values on the CPU.
391#ifdef WHITE_BALANCE
392 color = max(color_grading.balance * color, vec3(0.0));
393#endif
395 // Perform the "sectional" color grading: i.e. the color grading that
396 // applies individually to shadows, midtones, and highlights.
397#ifdef SECTIONAL_COLOR_GRADING
398 color = sectional_color_grading(color, &color_grading);
399#else
400 // If we're not doing sectional color grading, the exposure might still need
401 // to be applied, for example when using auto exposure.
402 color = color * powsafe(vec3(2.0), color_grading.exposure);
403#endif
405 // tone_mapping
406#ifdef TONEMAP_METHOD_NONE
407 color = color;
408#else ifdef TONEMAP_METHOD_REINHARD
409 color = tonemapping_reinhard(color.rgb);
410#else ifdef TONEMAP_METHOD_REINHARD_LUMINANCE
411 color = tonemapping_reinhard_luminance(color.rgb);
412#else ifdef TONEMAP_METHOD_ACES_FITTED
413 color = ACESFitted(color.rgb);
414#else ifdef TONEMAP_METHOD_AGX
415 color = applyAgXLog(color);
416 color = applyLUT3D(color, 32.0);
417#else ifdef TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM
418 color = somewhat_boring_display_transform(color.rgb);
419#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE
420 color = sample_tony_mc_mapface_lut(color);
421#else ifdef TONEMAP_METHOD_BLENDER_FILMIC
422 color = sample_blender_filmic_lut(color.rgb);
423#else ifdef TONEMAP_METHOD_PBR_NEUTRAL
424 color = tonemapping_pbr_neutral(color.rgb);
425#endif
427 // Perceptual post tonemapping grading
428 color = saturation(color, color_grading.post_saturation);
430 return vec4(color, in.a);
433// This is an **incredibly crude** approximation of the inverse of the tone mapping function.
434// We assume here that there's a simple linear relationship between the input and output
435// which is not true at all, but useful to at least preserve the overall luminance of colors
436// when sampling from an already tonemapped image. (e.g. for transmissive materials when HDR is off)
437fn approximate_inverse_tone_mapping(in: vec4<f32>, color_grading: ColorGrading) -> vec4<f32> {
438 let out = tone_mapping(in, color_grading);
439 let approximate_ratio = length(in.rgb) / length(out.rgb);
440 return vec4(in.rgb * approximate_ratio, in.a);
Contents