1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! Where linear light stops being light and becomes a picture.
//!
//! The 3D passes draw into a floating-point image so that a highlight can be
//! brighter than white -- see [`HDR_FORMAT`] -- and this pass fits that range
//! into what a display can show. It is the last thing drawn into the frame
//! before the UI, which is drawn over the top of it and is not tonemapped: a
//! label is not lit by anything, and a curve meant for light would only wash
//! it out.
//!
//! See `tonemap.wgsl` for the curve itself.
/// What the world is drawn into: half floats, so radiance above 1.0 survives
/// as far as the tonemapper instead of clipping in every pass on the way.
pub const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
/// The pass that reads the HDR image and writes the frame.
pub struct Tonemap {
pipeline: wgpu::RenderPipeline,
layout: wgpu::BindGroupLayout,
/// Bound to the HDR image, which is remade whenever the window resizes;
/// the size it was made for is how we notice.
bound: Option<(u32, u32, wgpu::BindGroup)>,
}
impl Tonemap {
/// `format` is the format of the frame being written -- the surface, an
/// sRGB format, which is what encodes the result on the way out.
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("tonemap shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("tonemap.wgsl").into()),
});
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("tonemap bind group layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
// Fetched pixel for pixel, so nothing filters it and the
// format never has to be filterable.
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("tonemap pipeline layout"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("tonemap pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
// The frame is written, not tested: nothing here is in front of
// or behind anything else.
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
Self {
pipeline,
layout,
bound: None,
}
}
/// Reads `hdr` and writes `target`, which is the whole of it: the
/// triangle covers the frame, so nothing is loaded first.
pub fn render(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
hdr: &wgpu::TextureView,
width: u32,
height: u32,
target: &wgpu::TextureView,
) {
if !matches!(&self.bound, Some((w, h, _)) if *w == width && *h == height) {
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("tonemap bind group"),
layout: &self.layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(hdr),
}],
});
self.bound = Some((width, height, bind_group));
}
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("tonemap pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bound.as_ref().expect("just bound").2, &[]);
pass.draw(0..3, 0..1);
}
}