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}