Skip to main content

polyscope_render/
ground_plane.rs

1//! Ground plane rendering.
2
3use wgpu::util::DeviceExt;
4
5/// GPU representation of ground plane uniforms.
6/// Matches the shader's `GroundUniforms` struct.
7#[repr(C)]
8#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
9pub struct GroundPlaneUniforms {
10    /// Scene center (xyz) + padding
11    pub center: [f32; 4],
12    /// Forward direction on ground plane (basis X)
13    pub basis_x: [f32; 4],
14    /// Right direction on ground plane (basis Y)
15    pub basis_y: [f32; 4],
16    /// Up direction / normal to ground (basis Z)
17    pub basis_z: [f32; 4],
18    /// Ground plane height
19    pub height: f32,
20    /// Scene length scale for tiling
21    pub length_scale: f32,
22    /// Camera height for fade calculation
23    pub camera_height: f32,
24    /// +1 or -1 depending on up direction
25    pub up_sign: f32,
26    /// Shadow darkness (0.0 = no shadow, 1.0 = full black)
27    pub shadow_darkness: f32,
28    /// Shadow mode: 0=none, `1=shadow_only`, `2=tile_with_shadow`
29    pub shadow_mode: u32,
30    /// Whether camera is in orthographic mode (0=perspective, 1=orthographic)
31    pub is_orthographic: u32,
32    /// Reflection intensity (0.0 = no reflection/opaque ground, 1.0 = mirror)
33    pub reflection_intensity: f32,
34}
35
36impl Default for GroundPlaneUniforms {
37    fn default() -> Self {
38        Self {
39            center: [0.0, 0.0, 0.0, 0.0],
40            basis_x: [0.0, 0.0, 1.0, 0.0], // Z forward
41            basis_y: [1.0, 0.0, 0.0, 0.0], // X right
42            basis_z: [0.0, 1.0, 0.0, 0.0], // Y up
43            height: 0.0,
44            length_scale: 1.0,
45            camera_height: 5.0,
46            up_sign: 1.0,
47            shadow_darkness: 0.4,
48            shadow_mode: 0, // No shadows by default
49            is_orthographic: 0,
50            reflection_intensity: 0.0, // No reflection by default
51        }
52    }
53}
54
55/// Ground plane render resources.
56pub struct GroundPlaneRenderData {
57    uniform_buffer: wgpu::Buffer,
58    bind_group: wgpu::BindGroup,
59}
60
61impl GroundPlaneRenderData {
62    /// Creates new ground plane render data.
63    ///
64    /// # Arguments
65    /// * `device` - The wgpu device
66    /// * `bind_group_layout` - The bind group layout
67    /// * `camera_buffer` - The camera uniform buffer
68    /// * `light_buffer` - The light uniform buffer (from `ShadowMapPass`)
69    /// * `shadow_depth_view` - The shadow map depth texture view
70    /// * `shadow_sampler` - The shadow comparison sampler
71    #[must_use]
72    pub fn new(
73        device: &wgpu::Device,
74        bind_group_layout: &wgpu::BindGroupLayout,
75        camera_buffer: &wgpu::Buffer,
76        light_buffer: &wgpu::Buffer,
77        shadow_depth_view: &wgpu::TextureView,
78        shadow_sampler: &wgpu::Sampler,
79    ) -> Self {
80        let uniforms = GroundPlaneUniforms::default();
81
82        let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
83            label: Some("Ground Plane Uniform Buffer"),
84            contents: bytemuck::cast_slice(&[uniforms]),
85            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
86        });
87
88        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
89            label: Some("Ground Plane Bind Group"),
90            layout: bind_group_layout,
91            entries: &[
92                wgpu::BindGroupEntry {
93                    binding: 0,
94                    resource: camera_buffer.as_entire_binding(),
95                },
96                wgpu::BindGroupEntry {
97                    binding: 1,
98                    resource: uniform_buffer.as_entire_binding(),
99                },
100                wgpu::BindGroupEntry {
101                    binding: 2,
102                    resource: light_buffer.as_entire_binding(),
103                },
104                wgpu::BindGroupEntry {
105                    binding: 3,
106                    resource: wgpu::BindingResource::TextureView(shadow_depth_view),
107                },
108                wgpu::BindGroupEntry {
109                    binding: 4,
110                    resource: wgpu::BindingResource::Sampler(shadow_sampler),
111                },
112            ],
113        });
114
115        Self {
116            uniform_buffer,
117            bind_group,
118        }
119    }
120
121    /// Updates the ground plane uniforms.
122    ///
123    /// # Arguments
124    /// * `queue` - The wgpu queue
125    /// * `scene_center` - Center of the scene bounding box
126    /// * `scene_min_y` - Minimum Y coordinate of scene bounding box
127    /// * `length_scale` - Scene length scale
128    /// * `camera_height` - Current camera Y position
129    /// * `height_override` - Optional manual height override
130    /// * `shadow_darkness` - Shadow darkness (0.0 = no shadow, 1.0 = full black)
131    /// * `shadow_mode` - Shadow mode: 0=none, `1=shadow_only`, `2=tile_with_shadow`
132    /// * `is_orthographic` - Whether camera is in orthographic mode
133    /// * `reflection_intensity` - Reflection intensity (0.0 = opaque, 1.0 = mirror)
134    #[allow(clippy::too_many_arguments)]
135    pub fn update(
136        &self,
137        queue: &wgpu::Queue,
138        scene_center: [f32; 3],
139        scene_min_y: f32,
140        length_scale: f32,
141        camera_height: f32,
142        height_override: Option<f32>,
143        shadow_darkness: f32,
144        shadow_mode: u32,
145        is_orthographic: bool,
146        reflection_intensity: f32,
147    ) {
148        // Compute ground height as offset from center
149        // The shader computes: center + up_direction * height
150        // So height should be relative to center, not absolute Y coordinate
151        let center_y = scene_center[1];
152        let height = height_override.unwrap_or_else(|| {
153            // Place at the scene's minimum Y coordinate
154            // Use a tiny offset (0.1% of length_scale) to avoid z-fighting
155            // height = (target_y - center_y), where target_y = scene_min_y - offset
156            let target_y = scene_min_y - length_scale * 0.001;
157            target_y - center_y
158        });
159
160        let uniforms = GroundPlaneUniforms {
161            center: [scene_center[0], scene_center[1], scene_center[2], 0.0],
162            // Y-up coordinate system: X=right, Z=forward, Y=up
163            basis_x: [0.0, 0.0, 1.0, 0.0], // Forward (Z)
164            basis_y: [1.0, 0.0, 0.0, 0.0], // Right (X)
165            basis_z: [0.0, 1.0, 0.0, 0.0], // Up (Y)
166            height,
167            length_scale,
168            camera_height,
169            up_sign: 1.0, // Y is up, so positive
170            shadow_darkness,
171            shadow_mode,
172            is_orthographic: u32::from(is_orthographic),
173            reflection_intensity,
174        };
175
176        queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
177    }
178
179    /// Returns the bind group for rendering.
180    #[must_use]
181    pub fn bind_group(&self) -> &wgpu::BindGroup {
182        &self.bind_group
183    }
184}