Skip to main content

renderling/
tonemapping.rs

1//! Tonemapping from an HDR texture to SDR.
2//!
3//! ## References
4//! * <https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/5b1b7f48a8cb2b7aaef00d08fdba18ccc8dd331b/source/Renderer/shaders/tonemapping.glsl>
5//! * <https://64.github.io/tonemapping>
6
7use crabslab::{Slab, SlabItem};
8use glam::{mat3, Mat3, Vec2, Vec3, Vec4, Vec4Swizzles};
9use spirv_std::{image::Image2d, spirv, Sampler};
10
11#[cfg(not(target_arch = "spirv"))]
12mod cpu;
13#[cfg(not(target_arch = "spirv"))]
14pub use cpu::*;
15
16const GAMMA: f32 = 2.2;
17const INV_GAMMA: f32 = 1.0 / GAMMA;
18
19/// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
20const ACESINPUT_MAT: Mat3 = mat3(
21    Vec3::new(0.59719, 0.07600, 0.02840),
22    Vec3::new(0.35458, 0.90834, 0.13383),
23    Vec3::new(0.04823, 0.01566, 0.83777),
24);
25
26/// ODT_SAT => XYZ => D60_2_D65 => sRGB
27const ACESOUTPUT_MAT: Mat3 = mat3(
28    Vec3::new(1.60475, -0.10208, -0.00327),
29    Vec3::new(-0.53108, 1.10813, -0.07276),
30    Vec3::new(-0.07367, -0.00605, 1.07602),
31);
32
33/// Linear to sRGB approximation.
34/// See <http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html>
35pub fn linear_to_srgb(color: Vec3) -> Vec3 {
36    color.powf(INV_GAMMA)
37}
38
39/// sRGB to linear approximation.
40/// See <http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html>
41pub fn srgb_to_linear(srgb_in: Vec3) -> Vec3 {
42    srgb_in.powf(GAMMA)
43}
44
45/// sRGB to linear approximation.
46/// See <http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html>
47pub fn srgba_to_linear(srgb_in: Vec4) -> Vec4 {
48    srgb_to_linear(srgb_in.xyz()).extend(srgb_in.w)
49}
50
51/// ACES tone map (faster approximation)
52/// see: <https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve>
53pub fn tone_map_aces_narkowicz(color: Vec3) -> Vec3 {
54    const A: f32 = 2.51;
55    const B: f32 = 0.03;
56    const C: f32 = 2.43;
57    const D: f32 = 0.59;
58    const E: f32 = 0.14;
59    let c = (color * (A * color + B)) / (color * (C * color + D) + E);
60    c.clamp(Vec3::ZERO, Vec3::ONE)
61}
62
63/// ACES filmic tone map approximation
64/// see <https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl>
65fn rrt_and_odtfit(color: Vec3) -> Vec3 {
66    let a: Vec3 = color * (color + 0.0245786) - 0.000090537;
67    let b: Vec3 = color * (0.983729 * color + 0.432951) + 0.238081;
68    a / b
69}
70
71pub fn tone_map_aces_hill(mut color: Vec3) -> Vec3 {
72    color = ACESINPUT_MAT * color;
73    // Apply RRT and ODT
74    color = rrt_and_odtfit(color);
75    color = ACESOUTPUT_MAT * color;
76    // Clamp to [0, 1]
77    color = color.clamp(Vec3::ZERO, Vec3::ONE);
78
79    color
80}
81
82pub fn tone_map_reinhard(color: Vec3) -> Vec3 {
83    color / (color + Vec3::ONE)
84}
85
86#[repr(transparent)]
87#[derive(Clone, Copy, Default, PartialEq, Eq, SlabItem, core::fmt::Debug)]
88pub struct Tonemap(u32);
89
90impl Tonemap {
91    pub const NONE: Self = Tonemap(0);
92    pub const ACES_NARKOWICZ: Self = Tonemap(1);
93    pub const ACES_HILL: Self = Tonemap(2);
94    pub const ACES_HILL_EXPOSURE_BOOST: Self = Tonemap(3);
95    pub const REINHARD: Self = Tonemap(4);
96}
97
98#[repr(C)]
99#[derive(Clone, Copy, PartialEq, SlabItem)]
100pub struct TonemapConstants {
101    pub tonemap: Tonemap,
102    pub exposure: f32,
103}
104
105impl Default for TonemapConstants {
106    fn default() -> Self {
107        Self {
108            tonemap: Tonemap::NONE,
109            exposure: 1.0,
110        }
111    }
112}
113
114pub fn tonemap(mut color: Vec4, slab: &[u32]) -> Vec4 {
115    let constants = slab.read::<TonemapConstants>(0u32.into());
116    color *= constants.exposure;
117
118    match constants.tonemap {
119        Tonemap::ACES_NARKOWICZ => tone_map_aces_narkowicz(color.xyz()).extend(color.w),
120        Tonemap::ACES_HILL => tone_map_aces_hill(color.xyz()).extend(color.w),
121        Tonemap::ACES_HILL_EXPOSURE_BOOST => {
122            // boost exposure as discussed in https://github.com/mrdoob/three.js/pull/19621
123            // this factor is based on the exposure correction of Krzysztof
124            // Narkowicz in his implemetation of ACES tone mapping
125            tone_map_aces_hill(color.xyz() / 0.6).extend(color.w)
126        }
127        Tonemap::REINHARD => {
128            // Use Reinhard tone mapping
129            tone_map_reinhard(color.xyz()).extend(color.w)
130        }
131        _ => color,
132    }
133}
134
135const QUAD_2D_POINTS: [(Vec2, Vec2); 6] = {
136    let tl = (Vec2::new(-1.0, 1.0), Vec2::new(0.0, 0.0));
137    let tr = (Vec2::new(1.0, 1.0), Vec2::new(1.0, 0.0));
138    let bl = (Vec2::new(-1.0, -1.0), Vec2::new(0.0, 1.0));
139    let br = (Vec2::new(1.0, -1.0), Vec2::new(1.0, 1.0));
140    [tl, bl, br, tl, br, tr]
141};
142
143#[spirv(vertex)]
144pub fn tonemapping_vertex(
145    #[spirv(vertex_index)] vertex_id: u32,
146    out_uv: &mut glam::Vec2,
147    #[spirv(position)] gl_pos: &mut glam::Vec4,
148) {
149    let (pos, uv) = QUAD_2D_POINTS[vertex_id as usize];
150    *out_uv = uv;
151    *gl_pos = pos.extend(0.0).extend(1.0);
152}
153
154#[spirv(fragment)]
155pub fn tonemapping_fragment(
156    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
157    #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,
158    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,
159    in_uv: glam::Vec2,
160    output: &mut glam::Vec4,
161) {
162    let color: Vec4 = texture.sample(*sampler, in_uv);
163    let color = tonemap(color, slab);
164    *output = color;
165}