1use std::sync::{Arc, Mutex};
5
6use viewport_lib::MeshId;
7use viewport_lib::resources::DeviceResources;
8use viewport_lib::runtime::{
9 GpuFrameContext, GpuPlugin, RuntimePlugin, RuntimeStepContext, gpu_phase, phase,
10};
11use viewport_lib::{DeformSlotHandle, DeformStage, DeformerDesc, DeformerId};
12
13use crate::field::{WindAuthoring, WindField};
14use crate::shader::{
15 WIND_DEFORMER_BODY, WIND_DEFORMER_NAME, WIND_MATERIAL_PARAMS_STRIDE_BYTES,
16 WIND_SWAY_STRIDE_BYTES,
17};
18
19#[derive(Clone, Copy, Debug)]
28pub struct WindMaterialParams {
29 pub strength: f32,
31 pub density: f32,
33 pub speed: f32,
35}
36
37impl Default for WindMaterialParams {
38 fn default() -> Self {
39 Self {
40 strength: 1.0,
41 density: 1.0,
42 speed: 1.0,
43 }
44 }
45}
46
47impl WindMaterialParams {
48 pub fn to_bytes(self) -> [u8; 12] {
50 let words: [f32; 3] = [self.strength, self.density, self.speed];
51 let mut out = [0u8; 12];
52 out.copy_from_slice(bytemuck::cast_slice(&words));
53 out
54 }
55}
56
57#[derive(Clone, Debug)]
64pub struct WindSwayWeights {
65 pub sway_mask: Vec<f32>,
67}
68
69impl WindSwayWeights {
70 pub fn uniform(vertex_count: usize, mask: f32) -> Self {
73 Self {
74 sway_mask: vec![mask; vertex_count],
75 }
76 }
77
78 pub fn height_falloff(positions: &[[f32; 3]], base_y: f32, top_y: f32) -> Self {
83 let range = top_y - base_y;
84 let sway_mask = if range.abs() < f32::EPSILON {
85 vec![0.0; positions.len()]
86 } else {
87 positions
88 .iter()
89 .map(|p| ((p[1] - base_y) / range).clamp(0.0, 1.0))
90 .collect()
91 };
92 Self { sway_mask }
93 }
94}
95
96struct WindInner {
100 field: WindField,
101 slot: Option<DeformSlotHandle>,
103}
104
105pub struct WindPlugin {
127 inner: Arc<Mutex<WindInner>>,
128}
129
130impl WindPlugin {
131 pub fn new(authoring: WindAuthoring) -> Self {
134 Self {
135 inner: Arc::new(Mutex::new(WindInner {
136 field: WindField::new(authoring),
137 slot: None,
138 })),
139 }
140 }
141
142 pub fn install(
151 &self,
152 resources: &mut DeviceResources,
153 device: &wgpu::Device,
154 ) -> Result<DeformerId, viewport_lib::ViewportError> {
155 let desc = DeformerDesc {
156 name: WIND_DEFORMER_NAME,
157 stage: DeformStage::WorldSpace,
158 priority: 0,
159 wgsl_body: WIND_DEFORMER_BODY.to_string(),
160 per_vertex_stride: WIND_SWAY_STRIDE_BYTES,
161 };
162 let id = resources.register_deformer(device, desc)?;
163 let handle = resources.deform_slot_handle(id);
164 self.inner.lock().expect("wind plugin poisoned").slot = Some(handle);
165 Ok(id)
166 }
167
168 pub fn attach_sway_mask(
174 &self,
175 resources: &mut DeviceResources,
176 device: &wgpu::Device,
177 mesh_id: MeshId,
178 weights: &WindSwayWeights,
179 ) -> bool {
180 let inner = self.inner.lock().expect("wind plugin poisoned");
181 let Some(handle) = inner.slot.as_ref() else {
182 return false;
183 };
184 let slot = handle.slot();
185 drop(inner);
186 resources.attach_deform_slot(
187 device,
188 mesh_id,
189 slot,
190 WIND_SWAY_STRIDE_BYTES,
191 bytemuck::cast_slice(&weights.sway_mask),
192 );
193 true
194 }
195
196 pub fn detach_sway_mask(
198 &self,
199 resources: &mut DeviceResources,
200 device: &wgpu::Device,
201 mesh_id: MeshId,
202 ) -> bool {
203 let slot = match self
204 .inner
205 .lock()
206 .expect("wind plugin poisoned")
207 .slot
208 .as_ref()
209 {
210 Some(h) => h.slot(),
211 None => return false,
212 };
213 resources.detach_deform_slot(device, mesh_id, slot)
214 }
215
216 pub fn attach_material_params(
230 &self,
231 resources: &mut DeviceResources,
232 device: &wgpu::Device,
233 queue: &wgpu::Queue,
234 mesh_id: MeshId,
235 params: &WindMaterialParams,
236 ) -> bool {
237 let inner = self.inner.lock().expect("wind plugin poisoned");
238 let Some(handle) = inner.slot.as_ref() else {
239 return false;
240 };
241 let slot = handle.slot();
242 drop(inner);
243 let bytes = params.to_bytes();
244 resources.attach_deform_slot_instance(
245 device,
246 queue,
247 mesh_id,
248 0,
249 slot,
250 WIND_MATERIAL_PARAMS_STRIDE_BYTES,
251 &bytes,
252 );
253 true
254 }
255
256 pub fn cpu_plugin(&self) -> impl RuntimePlugin {
258 WindCpuHalf {
259 inner: self.inner.clone(),
260 }
261 }
262
263 pub fn gpu_plugin(&self) -> impl GpuPlugin {
265 WindGpuHalf {
266 inner: self.inner.clone(),
267 }
268 }
269
270 pub fn set_authoring(&self, authoring: WindAuthoring) {
272 self.inner
273 .lock()
274 .expect("wind plugin poisoned")
275 .field
276 .set_authoring(authoring);
277 }
278
279 pub fn field(&self) -> WindField {
282 self.inner
283 .lock()
284 .expect("wind plugin poisoned")
285 .field
286 .clone()
287 }
288}
289
290struct WindCpuHalf {
291 inner: Arc<Mutex<WindInner>>,
292}
293
294impl RuntimePlugin for WindCpuHalf {
295 fn priority(&self) -> i32 {
296 phase::PREPARE
297 }
298
299 fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
300 self.inner
301 .lock()
302 .expect("wind plugin poisoned")
303 .field
304 .advance(ctx.dt);
305 }
306}
307
308struct WindGpuHalf {
309 inner: Arc<Mutex<WindInner>>,
310}
311
312impl GpuPlugin for WindGpuHalf {
313 fn priority(&self) -> i32 {
314 gpu_phase::PRE_PREPARE
315 }
316
317 fn pre_prepare(
318 &mut self,
319 _device: &wgpu::Device,
320 queue: &wgpu::Queue,
321 _ctx: &GpuFrameContext<'_>,
322 ) -> Vec<wgpu::CommandBuffer> {
323 let inner = self.inner.lock().expect("wind plugin poisoned");
324 if let Some(handle) = inner.slot.as_ref() {
325 let params = inner.field.to_slot_params();
326 handle.write(queue, ¶ms);
327 }
328 Vec::new()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use glam::Vec3;
336
337 #[test]
338 fn uniform_weights_have_right_length_and_value() {
339 let w = WindSwayWeights::uniform(5, 0.7);
340 assert_eq!(w.sway_mask.len(), 5);
341 assert!(w.sway_mask.iter().all(|&m| (m - 0.7).abs() < 1e-6));
342 }
343
344 #[test]
345 fn height_falloff_is_zero_at_base_and_one_at_top() {
346 let positions = vec![
347 [0.0, 0.0, 0.0],
348 [0.0, 1.0, 0.0],
349 [0.0, 2.0, 0.0],
350 [0.0, -1.0, 0.0],
351 [0.0, 5.0, 0.0],
352 ];
353 let w = WindSwayWeights::height_falloff(&positions, 0.0, 2.0);
354 assert_eq!(w.sway_mask[0], 0.0);
355 assert_eq!(w.sway_mask[1], 0.5);
356 assert_eq!(w.sway_mask[2], 1.0);
357 assert_eq!(w.sway_mask[3], 0.0);
358 assert_eq!(w.sway_mask[4], 1.0);
359 }
360
361 #[test]
362 fn height_falloff_degenerate_range_is_zero() {
363 let positions = vec![[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]];
364 let w = WindSwayWeights::height_falloff(&positions, 1.0, 1.0);
365 assert!(w.sway_mask.iter().all(|&m| m == 0.0));
366 }
367
368 fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
369 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
370 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
371 power_preference: wgpu::PowerPreference::LowPower,
372 compatible_surface: None,
373 force_fallback_adapter: false,
374 }))
375 .ok()?;
376 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
377 }
378
379 #[test]
380 fn install_registers_deformer_on_a_host_slot() {
381 let Some((device, _queue)) = try_make_device() else {
382 eprintln!("skipping: no wgpu adapter available");
383 return;
384 };
385 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb, 1);
386 let baseline = resources.registered_deformer_count();
388
389 let wind = WindPlugin::new(WindAuthoring::default());
390 let id = wind.install(&mut resources, &device).expect("install");
391
392 assert!(id.slot() < viewport_lib::DEFORM_SLOT_COUNT);
393 assert_eq!(resources.registered_deformer_count(), baseline + 1);
394 }
395
396 #[test]
397 fn attach_material_params_writes_per_instance_slot_data() {
398 let Some((device, queue)) = try_make_device() else {
399 eprintln!("skipping: no wgpu adapter available");
400 return;
401 };
402 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb, 1);
403 let plane = viewport_lib::geometry::primitives::grid_plane(1.0, 1.0, 4, 4);
404 let mesh_id = resources.upload_mesh_data(&device, &plane).unwrap();
405
406 let wind = WindPlugin::new(WindAuthoring::default());
407 let id = wind.install(&mut resources, &device).expect("install");
408
409 let params = WindMaterialParams {
410 strength: 0.2,
411 density: 3.0,
412 speed: 0.5,
413 };
414 assert!(wind.attach_material_params(&mut resources, &device, &queue, mesh_id, ¶ms));
415 assert!(resources.has_deform_slot_instance(mesh_id, 0, id.slot()));
416 }
417
418 #[test]
419 fn attach_sway_mask_marks_slot_active_for_mesh() {
420 let Some((device, _queue)) = try_make_device() else {
421 eprintln!("skipping: no wgpu adapter available");
422 return;
423 };
424 let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb, 1);
425 let plane = viewport_lib::geometry::primitives::grid_plane(1.0, 1.0, 4, 4);
426 let mesh_id = resources.upload_mesh_data(&device, &plane).unwrap();
427
428 let wind = WindPlugin::new(WindAuthoring::default());
429 let id = wind.install(&mut resources, &device).expect("install");
430
431 let mask = WindSwayWeights::uniform(plane.positions.len(), 1.0);
432 assert!(wind.attach_sway_mask(&mut resources, &device, mesh_id, &mask));
433 assert!(resources.has_deform_slot(mesh_id, id.slot()));
434
435 assert!(wind.detach_sway_mask(&mut resources, &device, mesh_id));
436 assert!(!resources.has_deform_slot(mesh_id, id.slot()));
437 }
438
439 #[test]
440 fn plugin_field_round_trips_authoring() {
441 let auth = WindAuthoring {
442 direction: Vec3::Z,
443 base_strength: 0.8,
444 gust_strength: 0.0,
445 gust_frequency: 0.0,
446 spatial_density: 0.0,
447 };
448 let p = WindPlugin::new(auth);
449 let sample = p.field().sample(Vec3::ZERO);
450 assert!((sample - Vec3::new(0.0, 0.0, 0.8)).length() < 1e-6);
451 }
452}