1use std::sync::Arc;
28
29use crate::context::GpuContext;
30use crate::error::{GpuError, GpuResult};
31use crate::shaders::{
32 ComputePipelineBuilder, WgslShader, create_compute_bind_group_layout, storage_buffer_layout,
33 uniform_buffer_layout,
34};
35
36use wgpu::{
37 BindGroupDescriptor, BindGroupEntry, BufferDescriptor, BufferUsages, CommandEncoderDescriptor,
38 ComputePassDescriptor,
39};
40
41#[derive(Debug, Clone)]
50pub struct RayMarchConfig {
51 pub view_dir: [f32; 3],
54
55 pub light_dir: [f32; 3],
58
59 pub step_size: f32,
62
63 pub max_steps: u32,
66
67 pub vertical_exaggeration: f32,
70
71 pub pixel_size_world: f32,
74}
75
76impl Default for RayMarchConfig {
77 fn default() -> Self {
78 let inv_sqrt3 = 1.0_f32 / 3.0_f32.sqrt();
79 Self {
80 view_dir: [0.0, 0.0, -1.0],
81 light_dir: [inv_sqrt3, inv_sqrt3, inv_sqrt3],
82 step_size: 0.5,
83 max_steps: 512,
84 vertical_exaggeration: 1.0,
85 pixel_size_world: 1.0,
86 }
87 }
88}
89
90impl RayMarchConfig {
91 pub fn validate(&self, width: u32, height: u32) -> GpuResult<()> {
100 if self.step_size <= 0.0 {
101 return Err(GpuError::invalid_kernel_params(format!(
102 "step_size must be > 0, got {}",
103 self.step_size
104 )));
105 }
106 if self.max_steps == 0 {
107 return Err(GpuError::invalid_kernel_params("max_steps must be > 0"));
108 }
109 if width == 0 {
110 return Err(GpuError::invalid_kernel_params("DEM width must be > 0"));
111 }
112 if height == 0 {
113 return Err(GpuError::invalid_kernel_params("DEM height must be > 0"));
114 }
115 Ok(())
116 }
117}
118
119pub struct RayMarchResult {
125 pub width: u32,
127 pub height: u32,
129
130 pub shaded: Vec<f32>,
134
135 pub depth: Vec<f32>,
141}
142
143#[repr(C, align(16))]
172#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
173struct RayMarchUniforms {
174 view_dir: [f32; 3],
175 _pad0: f32,
176 light_dir: [f32; 3],
177 step_size: f32,
178 max_steps: u32,
179 vertical_exaggeration: f32,
180 pixel_size_world: f32,
181 width: u32,
182 height: u32,
183 _pad_end: [u32; 3],
184}
185
186impl RayMarchUniforms {
187 fn from_config(cfg: &RayMarchConfig, width: u32, height: u32) -> Self {
188 Self {
189 view_dir: cfg.view_dir,
190 _pad0: 0.0,
191 light_dir: cfg.light_dir,
192 step_size: cfg.step_size,
193 max_steps: cfg.max_steps,
194 vertical_exaggeration: cfg.vertical_exaggeration,
195 pixel_size_world: cfg.pixel_size_world,
196 width,
197 height,
198 _pad_end: [0, 0, 0],
199 }
200 }
201}
202
203pub struct DemRayMarcher {
212 ctx: Arc<GpuContext>,
213 pipeline: Arc<wgpu::ComputePipeline>,
214 bind_group_layout: wgpu::BindGroupLayout,
215}
216
217impl DemRayMarcher {
218 pub fn new(ctx: Arc<GpuContext>) -> GpuResult<Self> {
225 let shader_src = make_ray_march_shader_source();
226 let mut shader = WgslShader::new(shader_src, "main");
227 let shader_module = shader.compile(ctx.device())?;
228
229 let bind_group_layout = create_compute_bind_group_layout(
235 ctx.device(),
236 &[
237 storage_buffer_layout(0, true), uniform_buffer_layout(1), storage_buffer_layout(2, false), storage_buffer_layout(3, false), ],
242 Some("DemRayMarcher Bind Group Layout"),
243 )?;
244
245 let pipeline = ComputePipelineBuilder::new(ctx.device(), shader_module, "main")
246 .bind_group_layout(&bind_group_layout)
247 .label("DemRayMarcher Pipeline")
248 .build()?;
249
250 Ok(Self {
251 ctx,
252 pipeline: Arc::new(pipeline),
253 bind_group_layout,
254 })
255 }
256
257 pub fn march(
273 &self,
274 dem: &[f32],
275 width: u32,
276 height: u32,
277 config: &RayMarchConfig,
278 ) -> GpuResult<RayMarchResult> {
279 config.validate(width, height)?;
280
281 let n_pixels = (width as usize) * (height as usize);
282 if dem.len() != n_pixels {
283 return Err(GpuError::invalid_kernel_params(format!(
284 "DEM length {} does not match width*height = {}",
285 dem.len(),
286 n_pixels
287 )));
288 }
289
290 let device = self.ctx.device();
291 let queue = self.ctx.queue();
292
293 let f32_size = std::mem::size_of::<f32>() as u64;
296 let dem_byte_size = (n_pixels as u64) * f32_size;
297 let out_byte_size = (n_pixels as u64) * f32_size;
298
299 let align = wgpu::COPY_BUFFER_ALIGNMENT;
301 let dem_aligned = ((dem_byte_size + align - 1) / align) * align;
302 let out_aligned = ((out_byte_size + align - 1) / align) * align;
303
304 let dem_buf = device.create_buffer(&BufferDescriptor {
306 label: Some("DemRayMarcher dem_buf"),
307 size: dem_aligned,
308 usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
309 mapped_at_creation: false,
310 });
311 queue.write_buffer(&dem_buf, 0, bytemuck::cast_slice(dem));
312
313 let uniforms = RayMarchUniforms::from_config(config, width, height);
315 let uniform_bytes: &[u8] = bytemuck::bytes_of(&uniforms);
316 let uniform_size = std::mem::size_of::<RayMarchUniforms>() as u64;
317 let uniform_aligned = ((uniform_size + align - 1) / align) * align;
319 let uniform_buf = device.create_buffer(&BufferDescriptor {
320 label: Some("DemRayMarcher uniform_buf"),
321 size: uniform_aligned,
322 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
323 mapped_at_creation: false,
324 });
325 queue.write_buffer(&uniform_buf, 0, uniform_bytes);
326
327 let shaded_buf = device.create_buffer(&BufferDescriptor {
329 label: Some("DemRayMarcher shaded_buf"),
330 size: out_aligned,
331 usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC,
332 mapped_at_creation: false,
333 });
334
335 let depth_buf = device.create_buffer(&BufferDescriptor {
337 label: Some("DemRayMarcher depth_buf"),
338 size: out_aligned,
339 usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC,
340 mapped_at_creation: false,
341 });
342
343 let shaded_staging = device.create_buffer(&BufferDescriptor {
345 label: Some("DemRayMarcher shaded_staging"),
346 size: out_aligned,
347 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
348 mapped_at_creation: false,
349 });
350 let depth_staging = device.create_buffer(&BufferDescriptor {
351 label: Some("DemRayMarcher depth_staging"),
352 size: out_aligned,
353 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
354 mapped_at_creation: false,
355 });
356
357 let bind_group = device.create_bind_group(&BindGroupDescriptor {
360 label: Some("DemRayMarcher BindGroup"),
361 layout: &self.bind_group_layout,
362 entries: &[
363 BindGroupEntry {
364 binding: 0,
365 resource: dem_buf.as_entire_binding(),
366 },
367 BindGroupEntry {
368 binding: 1,
369 resource: uniform_buf.as_entire_binding(),
370 },
371 BindGroupEntry {
372 binding: 2,
373 resource: shaded_buf.as_entire_binding(),
374 },
375 BindGroupEntry {
376 binding: 3,
377 resource: depth_buf.as_entire_binding(),
378 },
379 ],
380 });
381
382 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
385 label: Some("DemRayMarcher encoder"),
386 });
387
388 {
389 let mut cpass = encoder.begin_compute_pass(&ComputePassDescriptor {
390 label: Some("DemRayMarcher compute pass"),
391 timestamp_writes: None,
392 });
393 cpass.set_pipeline(&self.pipeline);
394 cpass.set_bind_group(0, &bind_group, &[]);
395
396 let wg_x = (width + 15) / 16;
398 let wg_y = (height + 15) / 16;
399 cpass.dispatch_workgroups(wg_x, wg_y, 1);
400 }
401
402 encoder.copy_buffer_to_buffer(&shaded_buf, 0, &shaded_staging, 0, out_aligned);
403 encoder.copy_buffer_to_buffer(&depth_buf, 0, &depth_staging, 0, out_aligned);
404
405 queue.submit(Some(encoder.finish()));
406
407 let shaded_slice = shaded_staging.slice(..);
409 let (shaded_tx, shaded_rx) = std::sync::mpsc::sync_channel(1);
410 shaded_slice.map_async(wgpu::MapMode::Read, move |result| {
411 let _ = shaded_tx.send(result);
412 });
413
414 let depth_slice = depth_staging.slice(..);
415 let (depth_tx, depth_rx) = std::sync::mpsc::sync_channel(1);
416 depth_slice.map_async(wgpu::MapMode::Read, move |result| {
417 let _ = depth_tx.send(result);
418 });
419
420 device
421 .poll(wgpu::PollType::wait_indefinitely())
422 .map_err(|e| GpuError::execution_failed(format!("device poll failed: {e}")))?;
423
424 let shaded_vec =
425 finish_staging_f32_read(&shaded_staging, shaded_slice, shaded_rx, n_pixels)?;
426 let depth_vec = finish_staging_f32_read(&depth_staging, depth_slice, depth_rx, n_pixels)?;
427
428 Ok(RayMarchResult {
429 width,
430 height,
431 shaded: shaded_vec,
432 depth: depth_vec,
433 })
434 }
435}
436
437fn finish_staging_f32_read(
441 staging: &wgpu::Buffer,
442 slice: wgpu::BufferSlice<'_>,
443 rx: std::sync::mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
444 count: usize,
445) -> GpuResult<Vec<f32>> {
446 rx.recv()
447 .map_err(|_| GpuError::buffer_mapping("staging channel closed unexpectedly"))?
448 .map_err(|e| GpuError::buffer_mapping(e.to_string()))?;
449
450 let mapped = slice.get_mapped_range();
451 let floats: &[f32] = bytemuck::cast_slice(&mapped[..count * 4]);
452 let out = floats.to_vec();
453 drop(mapped);
454 staging.unmap();
455 Ok(out)
456}
457
458pub fn normalize3(v: [f32; 3]) -> [f32; 3] {
475 let mag = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
476 if mag < 1e-10 {
477 [0.0, 0.0, 0.0]
478 } else {
479 [v[0] / mag, v[1] / mag, v[2] / mag]
480 }
481}
482
483pub fn sample_dem_bilinear(dem: &[f32], width: u32, height: u32, fx: f32, fy: f32) -> f32 {
508 let w = width as f32;
509 let h = height as f32;
510
511 let cx = fx.clamp(0.0, w - 1.0);
513 let cy = fy.clamp(0.0, h - 1.0);
514
515 let x0 = cx.floor() as u32;
516 let y0 = cy.floor() as u32;
517
518 let x1 = (x0 + 1).min(width - 1);
520 let y1 = (y0 + 1).min(height - 1);
521
522 let tx = cx - x0 as f32; let ty = cy - y0 as f32;
524
525 let w_u = width as usize;
526 let v00 = dem[y0 as usize * w_u + x0 as usize];
527 let v10 = dem[y0 as usize * w_u + x1 as usize];
528 let v01 = dem[y1 as usize * w_u + x0 as usize];
529 let v11 = dem[y1 as usize * w_u + x1 as usize];
530
531 let top = v00 + (v10 - v00) * tx;
533 let bot = v01 + (v11 - v01) * tx;
534 top + (bot - top) * ty
535}
536
537pub fn ray_march_cpu(
552 dem: &[f32],
553 width: u32,
554 height: u32,
555 config: &RayMarchConfig,
556) -> RayMarchResult {
557 let n_pixels = (width as usize) * (height as usize);
558 let mut shaded = vec![1.0_f32; n_pixels];
559 let mut depth = vec![1.0_f32; n_pixels];
560
561 let ld = config.light_dir;
562 let step = config.step_size;
563 let vert = config.vertical_exaggeration;
564 let max_s = config.max_steps;
565
566 for py in 0..height {
567 for px in 0..width {
568 let idx = (py as usize) * (width as usize) + (px as usize);
569
570 let start_x = px as f32;
571 let start_y = py as f32;
572 let start_z = dem[idx] * vert;
573
574 let mut shadowed = false;
575 let mut last_step = max_s; for s in 1..=max_s {
578 let sf = s as f32;
579 let sample_x = start_x + ld[0] * step * sf;
580 let sample_y = start_y + ld[1] * step * sf;
581 let terrain_z = sample_dem_bilinear(dem, width, height, sample_x, sample_y) * vert;
582 let ray_z = start_z + ld[2] * step * sf;
583
584 if terrain_z > ray_z {
585 shadowed = true;
586 last_step = s;
587 break;
588 }
589 }
590
591 if shadowed {
592 shaded[idx] = 0.0;
593 depth[idx] = (last_step as f32) / (max_s as f32);
595 } else {
596 shaded[idx] = 1.0;
597 depth[idx] = 1.0;
598 }
599 }
600 }
601
602 RayMarchResult {
603 width,
604 height,
605 shaded,
606 depth,
607 }
608}
609
610pub fn make_ray_march_shader_source() -> String {
626 r#"
627// ── Ray-march DEM shadow kernel ─────────────────────────────────────────────
628//
629// Bindings
630// 0 dem : array<f32> (read-only storage) — row-major DEM elevations
631// 1 config : RayMarchUniforms (uniform)
632// 2 shaded : array<f32> (read-write storage) — 0=shadow, 1=lit
633// 3 depth_buf : array<f32> (read-write storage) — normalised march depth
634//
635// Workgroup: 16 × 16 × 1
636
637struct RayMarchUniforms {
638 view_dir : vec3<f32>,
639 // _pad0 (4 bytes) implicitly added by vec3 alignment
640 light_dir : vec3<f32>,
641 // step_size follows light_dir at offset 28 (no extra pad — WGSL only pads
642 // vec3 when the *next* member is itself align-≥-16, e.g. another vec3).
643 step_size : f32,
644 max_steps : u32,
645 vertical_exaggeration : f32,
646 pixel_size_world : f32,
647 width : u32,
648 height : u32,
649 // _pad_end (12 bytes) — aligns total struct size to 16
650}
651
652@group(0) @binding(0) var<storage, read> dem : array<f32>;
653@group(0) @binding(1) var<uniform> config : RayMarchUniforms;
654@group(0) @binding(2) var<storage, read_write> shaded : array<f32>;
655@group(0) @binding(3) var<storage, read_write> depth_buf : array<f32>;
656
657// ── Bilinear sample of the DEM at fractional pixel coordinates ───────────────
658fn sample_dem(fx: f32, fy: f32) -> f32 {
659 let w = f32(config.width);
660 let h = f32(config.height);
661 let cx = clamp(fx, 0.0, w - 1.0);
662 let cy = clamp(fy, 0.0, h - 1.0);
663
664 let x0 = u32(cx);
665 let y0 = u32(cy);
666 let x1 = min(x0 + 1u, config.width - 1u);
667 let y1 = min(y0 + 1u, config.height - 1u);
668
669 let tx = cx - f32(x0);
670 let ty = cy - f32(y0);
671
672 let v00 = dem[y0 * config.width + x0];
673 let v10 = dem[y0 * config.width + x1];
674 let v01 = dem[y1 * config.width + x0];
675 let v11 = dem[y1 * config.width + x1];
676
677 let top = v00 + (v10 - v00) * tx;
678 let bot = v01 + (v11 - v01) * tx;
679 return top + (bot - top) * ty;
680}
681
682@compute @workgroup_size(16, 16, 1)
683fn main(@builtin(global_invocation_id) id: vec3<u32>) {
684 let px = id.x;
685 let py = id.y;
686
687 // Discard threads that fall outside the raster bounds.
688 if px >= config.width || py >= config.height {
689 return;
690 }
691
692 let idx = py * config.width + px;
693 let start_x = f32(px);
694 let start_y = f32(py);
695 let start_z = dem[idx] * config.vertical_exaggeration;
696
697 let ld = config.light_dir;
698 let step = config.step_size;
699 let vert = config.vertical_exaggeration;
700 let max_s = config.max_steps;
701
702 var shadow_flag : f32 = 1.0;
703 var depth_val : f32 = 1.0;
704
705 for (var s: u32 = 1u; s <= max_s; s = s + 1u) {
706 let sf = f32(s);
707 let sample_x = start_x + ld.x * step * sf;
708 let sample_y = start_y + ld.y * step * sf;
709 let terrain_z = sample_dem(sample_x, sample_y) * vert;
710 let ray_z = start_z + ld.z * step * sf;
711
712 if terrain_z > ray_z {
713 shadow_flag = 0.0;
714 depth_val = f32(s) / f32(max_s);
715 break;
716 }
717 }
718
719 shaded[idx] = shadow_flag;
720 depth_buf[idx] = depth_val;
721}
722"#
723 .to_string()
724}