1use crate::pipeline::{GpuMaterial, GpuPrimitive};
4use bytemuck::{Pod, Zeroable};
5use std::borrow::Cow;
6use wgpu::util::DeviceExt;
7
8#[repr(C)]
10#[derive(Clone, Copy, Debug, Pod, Zeroable)]
11pub struct CameraConfig {
12 pub width: u32,
13 pub height: u32,
14 pub samples_per_pixel: u32,
15 pub max_bounces: u32,
16 pub cam_pos: [f32; 3],
17 pub _pad0: f32,
18 pub cam_forward: [f32; 3],
19 pub _pad1: f32,
20 pub cam_right: [f32; 3],
21 pub _pad2: f32,
22 pub cam_up: [f32; 3],
23 pub fov_tan: f32,
24 pub num_primitives: u32,
25 pub seed_offset: u32,
26 pub source_intensity: f32,
27 pub source_radius: f32,
28 pub source_pos: [f32; 3],
29 pub _pad3: f32,
30 pub lvk_c_steps: u32,
31 pub lvk_g_steps: u32,
32 pub lvk_g_max: f32,
33 pub lvk_max_intensity: f32,
34}
35
36pub struct CameraImage {
38 pub width: u32,
39 pub height: u32,
40 pub pixels: Vec<[f32; 3]>, }
42
43impl CameraImage {
44 pub fn denoise(&mut self, strength: u32) {
47 let radius = strength.min(10) as i32;
48 let sigma_space = radius as f32 * 0.5;
49 let sigma_color = 0.15f32; let src = self.pixels.clone();
51 let w = self.width as i32;
52 let h = self.height as i32;
53
54 for y in 0..h {
55 for x in 0..w {
56 let idx = (y * w + x) as usize;
57 let center = src[idx];
58 let mut sum = [0.0f32; 3];
59 let mut weight_sum = 0.0f32;
60
61 for dy in -radius..=radius {
62 for dx in -radius..=radius {
63 let nx = x + dx;
64 let ny = y + dy;
65 if nx < 0 || nx >= w || ny < 0 || ny >= h {
66 continue;
67 }
68
69 let ni = (ny * w + nx) as usize;
70 let neighbor = src[ni];
71
72 let dist2 = (dx * dx + dy * dy) as f32;
74 let w_space = (-dist2 / (2.0 * sigma_space * sigma_space)).exp();
75
76 let cdiff = (center[0] - neighbor[0]).powi(2)
78 + (center[1] - neighbor[1]).powi(2)
79 + (center[2] - neighbor[2]).powi(2);
80 let w_color = (-cdiff / (2.0 * sigma_color * sigma_color)).exp();
81
82 let w = w_space * w_color;
83 sum[0] += neighbor[0] * w;
84 sum[1] += neighbor[1] * w;
85 sum[2] += neighbor[2] * w;
86 weight_sum += w;
87 }
88 }
89
90 if weight_sum > 0.0 {
91 self.pixels[idx] = [
92 sum[0] / weight_sum,
93 sum[1] / weight_sum,
94 sum[2] / weight_sum,
95 ];
96 }
97 }
98 }
99 }
100
101 pub fn to_srgb_bytes(&self) -> Vec<u8> {
103 self.to_srgb_bytes_with_exposure(1.0)
104 }
105
106 pub fn to_srgb_bytes_with_exposure(&self, exposure: f32) -> Vec<u8> {
108 let mut bytes = Vec::with_capacity(self.width as usize * self.height as usize * 4);
109 for pixel in &self.pixels {
110 let exposed = [
111 pixel[0] * exposure,
112 pixel[1] * exposure,
113 pixel[2] * exposure,
114 ];
115 let mapped = [
117 aces_tonemap(exposed[0]),
118 aces_tonemap(exposed[1]),
119 aces_tonemap(exposed[2]),
120 ];
121 for c in &mapped {
123 let srgb = if *c <= 0.0031308 {
124 c * 12.92
125 } else {
126 1.055 * c.powf(1.0 / 2.4) - 0.055
127 };
128 bytes.push((srgb.clamp(0.0, 1.0) * 255.0) as u8);
129 }
130 bytes.push(255);
131 }
132 bytes
133 }
134}
135
136fn aces_tonemap(x: f32) -> f32 {
138 let a = 2.51;
139 let b = 0.03;
140 let c = 2.43;
141 let d = 0.59;
142 let e = 0.14;
143 ((x * (a * x + b)) / (x * (c * x + d) + e)).clamp(0.0, 1.0)
144}
145
146pub struct GpuCamera {
148 device: wgpu::Device,
149 queue: wgpu::Queue,
150 pipeline: wgpu::ComputePipeline,
151 bind_group_layout: wgpu::BindGroupLayout,
152}
153
154impl GpuCamera {
155 pub async fn new() -> Result<Self, String> {
157 let instance = wgpu::Instance::default();
158 let adapter = instance
159 .request_adapter(&wgpu::RequestAdapterOptions {
160 power_preference: wgpu::PowerPreference::HighPerformance,
161 ..default()
162 })
163 .await
164 .map_err(|e| format!("No GPU: {e}"))?;
165
166 let (device, queue) = adapter
167 .request_device(&wgpu::DeviceDescriptor {
168 label: Some("eulumdat-rt-camera"),
169 required_features: wgpu::Features::empty(),
170 required_limits: wgpu::Limits::default(),
171 ..Default::default()
172 })
173 .await
174 .map_err(|e| format!("Device: {e}"))?;
175
176 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
177 label: Some("camera.wgsl"),
178 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/camera.wgsl"))),
179 });
180
181 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
182 label: Some("camera_bgl"),
183 entries: &[
184 wgpu::BindGroupLayoutEntry {
186 binding: 0,
187 visibility: wgpu::ShaderStages::COMPUTE,
188 ty: wgpu::BindingType::Buffer {
189 ty: wgpu::BufferBindingType::Storage { read_only: false },
190 has_dynamic_offset: false,
191 min_binding_size: None,
192 },
193 count: None,
194 },
195 wgpu::BindGroupLayoutEntry {
197 binding: 1,
198 visibility: wgpu::ShaderStages::COMPUTE,
199 ty: wgpu::BindingType::Buffer {
200 ty: wgpu::BufferBindingType::Uniform,
201 has_dynamic_offset: false,
202 min_binding_size: None,
203 },
204 count: None,
205 },
206 wgpu::BindGroupLayoutEntry {
208 binding: 2,
209 visibility: wgpu::ShaderStages::COMPUTE,
210 ty: wgpu::BindingType::Buffer {
211 ty: wgpu::BufferBindingType::Storage { read_only: true },
212 has_dynamic_offset: false,
213 min_binding_size: None,
214 },
215 count: None,
216 },
217 wgpu::BindGroupLayoutEntry {
219 binding: 3,
220 visibility: wgpu::ShaderStages::COMPUTE,
221 ty: wgpu::BindingType::Buffer {
222 ty: wgpu::BufferBindingType::Storage { read_only: true },
223 has_dynamic_offset: false,
224 min_binding_size: None,
225 },
226 count: None,
227 },
228 wgpu::BindGroupLayoutEntry {
230 binding: 4,
231 visibility: wgpu::ShaderStages::COMPUTE,
232 ty: wgpu::BindingType::Buffer {
233 ty: wgpu::BufferBindingType::Storage { read_only: true },
234 has_dynamic_offset: false,
235 min_binding_size: None,
236 },
237 count: None,
238 },
239 ],
240 });
241
242 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
243 label: Some("camera_pl"),
244 bind_group_layouts: &[Some(&bind_group_layout)],
245 immediate_size: 0,
246 });
247
248 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
249 label: Some("camera_pipeline"),
250 layout: Some(&pipeline_layout),
251 module: &shader,
252 entry_point: Some("trace_camera"),
253 compilation_options: Default::default(),
254 cache: None,
255 });
256
257 Ok(Self {
258 device,
259 queue,
260 pipeline,
261 bind_group_layout,
262 })
263 }
264
265 #[allow(clippy::too_many_arguments)]
270 pub async fn render(
271 &self,
272 width: u32,
273 height: u32,
274 samples_per_pixel: u32,
275 camera_pos: [f32; 3],
276 look_at: [f32; 3],
277 fov_degrees: f32,
278 primitives: &[GpuPrimitive],
279 materials: &[GpuMaterial],
280 source_intensity: f32,
281 source_pos: [f32; 3],
282 ) -> CameraImage {
283 self.render_with_lvk(
284 width,
285 height,
286 samples_per_pixel,
287 camera_pos,
288 look_at,
289 fov_degrees,
290 primitives,
291 materials,
292 source_intensity,
293 source_pos,
294 &[],
295 0,
296 0,
297 0.0,
298 0.0,
299 )
300 .await
301 }
302
303 #[allow(clippy::too_many_arguments)]
305 pub async fn render_with_lvk(
306 &self,
307 width: u32,
308 height: u32,
309 samples_per_pixel: u32,
310 camera_pos: [f32; 3],
311 look_at: [f32; 3],
312 fov_degrees: f32,
313 primitives: &[GpuPrimitive],
314 materials: &[GpuMaterial],
315 source_intensity: f32,
316 source_pos: [f32; 3],
317 lvk_data: &[f32],
318 lvk_c_steps: u32,
319 lvk_g_steps: u32,
320 lvk_g_max: f32,
321 lvk_max_intensity: f32,
322 ) -> CameraImage {
323 let pos = glam::Vec3::from(camera_pos);
325 let target = glam::Vec3::from(look_at);
326 let forward = (target - pos).normalize();
327 let world_up = glam::Vec3::Y;
328 let right = forward.cross(world_up).normalize();
329 let up = right.cross(forward).normalize();
330
331 let config = CameraConfig {
332 width,
333 height,
334 samples_per_pixel,
335 max_bounces: 8,
336 cam_pos: camera_pos,
337 _pad0: 0.0,
338 cam_forward: forward.to_array(),
339 _pad1: 0.0,
340 cam_right: right.to_array(),
341 _pad2: 0.0,
342 cam_up: up.to_array(),
343 fov_tan: (fov_degrees.to_radians() / 2.0).tan(),
344 num_primitives: primitives.len() as u32,
345 seed_offset: 42,
346 source_intensity,
347 source_radius: 0.02,
348 source_pos,
349 _pad3: 0.0,
350 lvk_c_steps,
351 lvk_g_steps,
352 lvk_g_max,
353 lvk_max_intensity,
354 };
355
356 let total_pixels = width * height;
357 let pixel_buffer_size = (total_pixels * 4) as u64 * 4;
359
360 let config_buf = self
361 .device
362 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
363 label: Some("cam_config"),
364 contents: bytemuck::bytes_of(&config),
365 usage: wgpu::BufferUsages::UNIFORM,
366 });
367
368 let pixel_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
369 label: Some("pixel_buf"),
370 size: pixel_buffer_size,
371 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
372 mapped_at_creation: false,
373 });
374
375 let readback_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
376 label: Some("readback_buf"),
377 size: pixel_buffer_size,
378 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
379 mapped_at_creation: false,
380 });
381
382 let dummy_prim = GpuPrimitive {
384 ptype: 0,
385 material_id: 0,
386 _pad0: 0,
387 _pad1: 0,
388 params: [0.0; 12],
389 };
390 let dummy_mat = GpuMaterial {
391 mtype: 0,
392 _pad0: 0,
393 _pad1: 0,
394 _pad2: 0,
395 reflectance: 0.0,
396 ior: 1.0,
397 transmittance: 0.0,
398 min_reflectance: 0.0,
399 absorption_coeff: 0.0,
400 scattering_coeff: 0.0,
401 asymmetry: 0.0,
402 thickness: 0.0,
403 };
404
405 let prim_data: Vec<GpuPrimitive> = if primitives.is_empty() {
406 vec![dummy_prim]
407 } else {
408 primitives.to_vec()
409 };
410 let mat_data: Vec<GpuMaterial> = if materials.is_empty() {
411 vec![dummy_mat]
412 } else {
413 materials.to_vec()
414 };
415
416 let prim_buf = self
417 .device
418 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
419 label: Some("cam_prims"),
420 contents: bytemuck::cast_slice(&prim_data),
421 usage: wgpu::BufferUsages::STORAGE,
422 });
423 let mat_buf = self
424 .device
425 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
426 label: Some("cam_mats"),
427 contents: bytemuck::cast_slice(&mat_data),
428 usage: wgpu::BufferUsages::STORAGE,
429 });
430
431 let lvk_buf_data: Vec<f32> = if lvk_data.is_empty() {
433 vec![1.0]
434 } else {
435 lvk_data.to_vec()
436 };
437 let lvk_buf = self
438 .device
439 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
440 label: Some("cam_lvk"),
441 contents: bytemuck::cast_slice(&lvk_buf_data),
442 usage: wgpu::BufferUsages::STORAGE,
443 });
444
445 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
446 label: Some("cam_bg"),
447 layout: &self.bind_group_layout,
448 entries: &[
449 wgpu::BindGroupEntry {
450 binding: 0,
451 resource: pixel_buf.as_entire_binding(),
452 },
453 wgpu::BindGroupEntry {
454 binding: 1,
455 resource: config_buf.as_entire_binding(),
456 },
457 wgpu::BindGroupEntry {
458 binding: 2,
459 resource: prim_buf.as_entire_binding(),
460 },
461 wgpu::BindGroupEntry {
462 binding: 3,
463 resource: mat_buf.as_entire_binding(),
464 },
465 wgpu::BindGroupEntry {
466 binding: 4,
467 resource: lvk_buf.as_entire_binding(),
468 },
469 ],
470 });
471
472 let wg_x = width.div_ceil(16);
474 let wg_y = height.div_ceil(16);
475
476 let mut encoder = self
477 .device
478 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
479 label: Some("cam_encoder"),
480 });
481
482 {
483 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
484 label: Some("cam_pass"),
485 timestamp_writes: None,
486 });
487 pass.set_pipeline(&self.pipeline);
488 pass.set_bind_group(0, &bind_group, &[]);
489 pass.dispatch_workgroups(wg_x, wg_y, 1);
490 }
491
492 encoder.copy_buffer_to_buffer(&pixel_buf, 0, &readback_buf, 0, pixel_buffer_size);
493 self.queue.submit(Some(encoder.finish()));
494
495 let slice = readback_buf.slice(..);
497 let (tx, rx) = flume::bounded(1);
498 slice.map_async(wgpu::MapMode::Read, move |r| {
499 tx.send(r).unwrap();
500 });
501 self.device.poll(wgpu::PollType::wait_indefinitely()).ok();
502 rx.recv_async().await.unwrap().unwrap();
503
504 let data = slice.get_mapped_range();
505 let raw: &[u32] = bytemuck::cast_slice(&data);
506
507 let mut pixels = vec![[0.0f32; 3]; total_pixels as usize];
508 for i in 0..total_pixels as usize {
509 let r = raw[i * 4] as f32 / 1000.0;
510 let g = raw[i * 4 + 1] as f32 / 1000.0;
511 let b = raw[i * 4 + 2] as f32 / 1000.0;
512 let count = raw[i * 4 + 3].max(1) as f32;
513 pixels[i] = [r / count, g / count, b / count];
514 }
515
516 drop(data);
517 readback_buf.unmap();
518
519 CameraImage {
520 width,
521 height,
522 pixels,
523 }
524 }
525}
526
527fn default() -> wgpu::RequestAdapterOptions<'static, 'static> {
528 wgpu::RequestAdapterOptions::default()
529}