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#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
88#[derive(Clone, Copy, Default, PartialEq, Eq, SlabItem)]
89pub struct Tonemap(u32);
90
91impl Tonemap {
92    pub const NONE: Self = Tonemap(0);
93    pub const ACES_NARKOWICZ: Self = Tonemap(1);
94    pub const ACES_HILL: Self = Tonemap(2);
95    pub const ACES_HILL_EXPOSURE_BOOST: Self = Tonemap(3);
96    pub const REINHARD: Self = Tonemap(4);
97}
98
99#[repr(C)]
100#[derive(Clone, Copy, PartialEq, SlabItem)]
101pub struct TonemapConstants {
102    pub tonemap: Tonemap,
103    pub exposure: f32,
104}
105
106impl Default for TonemapConstants {
107    fn default() -> Self {
108        Self {
109            tonemap: Tonemap::NONE,
110            exposure: 1.0,
111        }
112    }
113}
114
115pub fn tonemap(mut color: Vec4, slab: &[u32]) -> Vec4 {
116    let constants = slab.read::<TonemapConstants>(0u32.into());
117    color *= constants.exposure;
118
119    match constants.tonemap {
120        Tonemap::ACES_NARKOWICZ => tone_map_aces_narkowicz(color.xyz()).extend(color.w),
121        Tonemap::ACES_HILL => tone_map_aces_hill(color.xyz()).extend(color.w),
122        Tonemap::ACES_HILL_EXPOSURE_BOOST => {
123            // boost exposure as discussed in https://github.com/mrdoob/three.js/pull/19621
124            // this factor is based on the exposure correction of Krzysztof Narkowicz in his
125            // implemetation of ACES tone mapping
126            tone_map_aces_hill(color.xyz() / 0.6).extend(color.w)
127        }
128        Tonemap::REINHARD => {
129            // Use Reinhard tone mapping
130            tone_map_reinhard(color.xyz()).extend(color.w)
131        }
132        _ => color,
133    }
134}
135
136const QUAD_2D_POINTS: [(Vec2, Vec2); 6] = {
137    let tl = (Vec2::new(-1.0, 1.0), Vec2::new(0.0, 0.0));
138    let tr = (Vec2::new(1.0, 1.0), Vec2::new(1.0, 0.0));
139    let bl = (Vec2::new(-1.0, -1.0), Vec2::new(0.0, 1.0));
140    let br = (Vec2::new(1.0, -1.0), Vec2::new(1.0, 1.0));
141    [tl, bl, br, tl, br, tr]
142};
143
144#[spirv(vertex)]
145pub fn tonemapping_vertex(
146    #[spirv(vertex_index)] vertex_id: u32,
147    out_uv: &mut glam::Vec2,
148    #[spirv(position)] gl_pos: &mut glam::Vec4,
149) {
150    let (pos, uv) = QUAD_2D_POINTS[vertex_id as usize];
151    *out_uv = uv;
152    *gl_pos = pos.extend(0.0).extend(1.0);
153}
154
155#[spirv(fragment)]
156pub fn tonemapping_fragment(
157    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
158    #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,
159    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,
160    in_uv: glam::Vec2,
161    output: &mut glam::Vec4,
162) {
163    let color: Vec4 = texture.sample(*sampler, in_uv);
164    let color = tonemap(color, slab);
165    *output = color;
166}