Skip to main content

ferrum_wgpu/
lib.rs

1pub mod assets;
2pub mod config;
3mod error;
4pub mod math;
5mod renderer;
6mod scene;
7
8use {
9    crate::{
10        assets::{
11            DrawLight, DrawModel, DrawShadow, InstanceRaw, Model, ModelDesc, ModelStore,
12            ModelVertex, Vertex,
13        },
14        config::WindowSize,
15        renderer::{CameraRig, Material, ShadowRig, SkyRig},
16        scene::{Light, LightRig, WindRig},
17    },
18    std::sync::Arc,
19    wgpu::{
20        Adapter, BindGroupLayout, CommandEncoder, Device, PipelineLayout, Queue, RenderPass,
21        RenderPipeline, Surface, SurfaceCapabilities, SurfaceConfiguration, SurfaceTexture,
22        TextureFormat, TextureView,
23    },
24};
25pub use {
26    assets::{Ingot, Instance, TypeModel},
27    cgmath::{Deg, Matrix4, Point3, Quaternion, Rotation3, Vector3, ortho},
28    error::SurfaceError,
29    winit::{dpi::PhysicalSize, keyboard::KeyCode},
30};
31
32pub struct State {
33    pub window_surface: wgpu::Surface<'static>,
34    pub device: Arc<wgpu::Device>,
35    pub queue: Arc<wgpu::Queue>,
36    pub config: wgpu::SurfaceConfiguration,
37    pub is_surface_configuration: bool,
38    pub render_pipeline: wgpu::RenderPipeline,
39    pub texture_bind_group_layout: Arc<wgpu::BindGroupLayout>,
40    pub depth_texture: renderer::Texture,
41    pub last_render_time: web_time::Instant,
42    pub camera: CameraRig,
43    pub light: LightRig,
44    pub wind: WindRig,
45    pub shadow: ShadowRig,
46    pub sky: SkyRig,
47    pub(crate) models: ModelStore,
48}
49
50impl State {
51    pub async fn new(
52        target: impl raw_window_handle::HasWindowHandle
53        + raw_window_handle::HasDisplayHandle
54        + wgpu::WasmNotSendSync
55        + 'static,
56        window_size: WindowSize,
57    ) -> anyhow::Result<Self> {
58        let mut instance_desc: wgpu::InstanceDescriptor =
59            wgpu::InstanceDescriptor::new_without_display_handle();
60        #[cfg(target_arch = "wasm32")]
61        {
62            instance_desc.backends = wgpu::Backends::GL | wgpu::Backends::BROWSER_WEBGPU;
63        }
64        #[cfg(all(not(target_arch = "wasm32"), not(feature = "rpi")))]
65        {
66            instance_desc.backends = wgpu::Backends::PRIMARY;
67        }
68        #[cfg(all(not(target_arch = "wasm32"), feature = "rpi"))]
69        {
70            instance_desc.backends = wgpu::Backends::GL;
71        }
72        let backend_instance: wgpu::Instance = wgpu::Instance::new(instance_desc);
73
74        // Surface to be drawn
75        let window_surface: Surface = backend_instance.create_surface(target)?;
76
77        // Representation of the system's physical GPU
78        let adapter: Adapter = backend_instance
79            .request_adapter(&wgpu::RequestAdapterOptions {
80                power_preference: wgpu::PowerPreference::default(),
81                force_fallback_adapter: false,
82                compatible_surface: Some(&window_surface),
83            })
84            .await?;
85
86        // Logic interface for creating resources and a command queue that is sent to the GPU
87        let (device, queue) = adapter
88            .request_device(&wgpu::DeviceDescriptor {
89                label: None,
90                // The engine uses no optional features. all_webgpu_mask() would demand
91                // every WebGPU feature as required,
92                required_features: wgpu::Features::empty(),
93                // The engine requires WebGPU (compute shader for the HDR cubemap) and never
94                // runs on WebGL2, so use the adapter's real limits on every target.
95                // downlevel_webgl2_defaults() would cap compute limits at 0 and break the
96                // equirect→cubemap compute pass.
97                required_limits: adapter.limits(),
98                experimental_features: wgpu::ExperimentalFeatures::disabled(),
99                memory_hints: Default::default(),
100                trace: wgpu::Trace::Off,
101            })
102            .await?;
103        let device: Arc<Device> = Arc::new(device);
104        let queue: Arc<Queue> = Arc::new(queue);
105
106        // A dynamic query of the capabilities that varies according to the adapter you have
107        let surface_caps: SurfaceCapabilities = window_surface.get_capabilities(&adapter);
108
109        // Define how pixels are stored in memory
110        let surface_format: TextureFormat = surface_caps
111            .formats
112            .iter()
113            .find(|f| f.is_srgb())
114            .copied()
115            .unwrap_or(surface_caps.formats[0]);
116
117        // Describe the surface configuration, which includes the format, size, and present mode
118        let config: SurfaceConfiguration = wgpu::SurfaceConfiguration {
119            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
120            format: surface_format,
121            width: window_size.width,
122            height: window_size.height,
123            present_mode: surface_caps.present_modes[0],
124            desired_maximum_frame_latency: 2,
125            alpha_mode: surface_caps.alpha_modes[0],
126            view_formats: vec![surface_format.add_srgb_suffix()],
127        };
128
129        // Each subsystem builds its own GPU resources; State only wires the
130        // layouts they need from one another.
131        let texture_bind_group_layout: Arc<BindGroupLayout> =
132            Arc::new(Material::bind_group_layout(&device));
133
134        let camera: CameraRig = CameraRig::new(&device, config.width as f32 / config.height as f32);
135
136        let depth_texture: renderer::Texture =
137            renderer::Texture::create_depth_texture(&device, &config, "depth_texture");
138
139        let sky: SkyRig = SkyRig::new(&device, &queue, &config, &camera.layout).await?;
140
141        let light: LightRig = LightRig::new(
142            &device,
143            &camera.layout,
144            &texture_bind_group_layout,
145            sky.hdr.format(),
146        );
147
148        let shadow: ShadowRig = ShadowRig::new(&device, &light.layout);
149
150        let wind: WindRig = WindRig::new(&device);
151
152        // Main render pipeline (textured geometry with light, shadow and wind)
153        let pipeline_render_layout: PipelineLayout =
154            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
155                bind_group_layouts: &[
156                    Some(&texture_bind_group_layout),
157                    Some(&camera.layout),
158                    Some(&light.layout),
159                    Some(&shadow.layout),
160                    Some(&wind.layout),
161                ],
162                label: Some("render_pipeline_layout"),
163                ..Default::default()
164            });
165
166        let render_pipeline: RenderPipeline = renderer::create_render_pipeline(
167            &device,
168            &pipeline_render_layout,
169            sky.hdr.format(),
170            Some(renderer::Texture::DEPTH_FORMAT),
171            &[ModelVertex::desc(), InstanceRaw::desc()],
172            wgpu::PrimitiveTopology::TriangleList,
173            wgpu::include_wgsl!("shaders/shaders.wgsl"),
174            wgpu::CompareFunction::Less,
175        );
176
177        Ok(Self {
178            window_surface,
179            device,
180            queue,
181            config,
182            is_surface_configuration: false,
183            render_pipeline,
184            texture_bind_group_layout,
185            depth_texture,
186            last_render_time: web_time::Instant::now(),
187            camera,
188            light,
189            wind,
190            shadow,
191            sky,
192            models: ModelStore::new(),
193        })
194    }
195
196    pub fn resize(&mut self, height: u32, width: u32) {
197        if height > 0 && width > 0 {
198            self.config.height = height;
199            self.config.width = width;
200
201            self.window_surface.configure(&self.device, &self.config);
202
203            self.camera
204                .set_aspect(self.config.width as f32 / self.config.height as f32);
205
206            self.depth_texture = renderer::Texture::create_depth_texture(
207                &self.device,
208                &self.config,
209                "depth_texture",
210            );
211
212            self.sky.hdr.resize(&self.device, width, height);
213            self.is_surface_configuration = true;
214        }
215    }
216
217    pub fn render(&mut self) -> Result<(), SurfaceError> {
218        self.render_with_overlay(&mut |_, _, _, _| {})
219    }
220
221    pub fn render_with_overlay(
222        &mut self,
223        overlay: &mut dyn FnMut(
224            &wgpu::Device,
225            &wgpu::Queue,
226            &mut wgpu::CommandEncoder,
227            &wgpu::TextureView,
228        ),
229    ) -> Result<(), SurfaceError> {
230        if !self.is_surface_configuration {
231            return Ok(());
232        }
233
234        let mut encoder: CommandEncoder =
235            self.device
236                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
237                    label: Some("encoder"),
238                });
239
240        {
241            let mut shadow_render_pass: RenderPass =
242                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
243                    label: Some("Shadow_render_pass"),
244                    color_attachments: &[],
245                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
246                        view: &self.shadow.texture.view,
247                        depth_ops: Some(wgpu::Operations {
248                            load: wgpu::LoadOp::Clear(1.0),
249                            store: wgpu::StoreOp::Store,
250                        }),
251                        stencil_ops: None,
252                    }),
253                    timestamp_writes: None,
254                    occlusion_query_set: None,
255                    multiview_mask: None,
256                });
257
258            shadow_render_pass.set_pipeline(&self.shadow.pipeline);
259            shadow_render_pass.set_bind_group(0, &self.light.bind_group, &[]);
260            for model in self.models.static_loaded() {
261                shadow_render_pass.draw_shadow_model(model, &self.light.bind_group);
262            }
263        }
264        {
265            let mut render_pass: RenderPass =
266                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
267                    label: Some("render_pass"),
268                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
269                        view: self.sky.hdr.view(),
270                        depth_slice: None,
271                        resolve_target: None,
272                        ops: wgpu::Operations {
273                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
274                            store: wgpu::StoreOp::Store,
275                        },
276                    })],
277                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
278                        view: &self.depth_texture.view,
279                        depth_ops: Some(wgpu::Operations {
280                            load: wgpu::LoadOp::Clear(1.0),
281                            store: wgpu::StoreOp::Store,
282                        }),
283                        stencil_ops: None,
284                    }),
285                    timestamp_writes: None,
286                    occlusion_query_set: None,
287                    multiview_mask: None,
288                });
289
290            render_pass.set_pipeline(&self.light.pipeline);
291            for model in self.models.light_loaded() {
292                render_pass.draw_light_model(
293                    model,
294                    &self.camera.bind_group,
295                    &self.light.bind_group,
296                );
297            }
298
299            render_pass.set_pipeline(&self.render_pipeline);
300            for model in self.models.static_loaded() {
301                render_pass.draw_model(
302                    model,
303                    &self.camera.bind_group,
304                    &self.light.bind_group,
305                    &self.shadow.bind_group,
306                    &self.wind.bind_group,
307                );
308            }
309
310            // Sky pipeline last: leverages the depth test (LessEqual with z=1.0)
311            // to paint only the pixels where no geometry was drawn.
312            render_pass.set_pipeline(&self.sky.pipeline);
313            render_pass.set_bind_group(0, &self.camera.bind_group, &[]);
314            render_pass.set_bind_group(1, &self.sky.bind_group, &[]);
315            render_pass.draw(0..3, 0..1);
316        }
317
318        let ouput: SurfaceTexture = match self.window_surface.get_current_texture() {
319            wgpu::CurrentSurfaceTexture::Success(t)
320            | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
321            // Frame no disponible temporalmente (minimizada, timeout): se salta
322            // el frame sin tratarlo como error.
323            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
324                return Ok(());
325            }
326            wgpu::CurrentSurfaceTexture::Outdated => return Err(SurfaceError::Outdated),
327            wgpu::CurrentSurfaceTexture::Lost => return Err(SurfaceError::Lost),
328            wgpu::CurrentSurfaceTexture::Validation => return Err(SurfaceError::Validation),
329        };
330        let view: TextureView = ouput.texture.create_view(&wgpu::TextureViewDescriptor {
331            format: Some(self.config.format.add_srgb_suffix()),
332            ..Default::default()
333        });
334
335        self.sky.hdr.process(&mut encoder, &view);
336        overlay(&self.device, &self.queue, &mut encoder, &view);
337        self.queue.submit(std::iter::once(encoder.finish()));
338
339        ouput.present();
340
341        Ok(())
342    }
343
344    pub fn spawn_model(&mut self, model_desc: ModelDesc) -> Ingot<Model> {
345        self.models.spawn(
346            &self.device,
347            &self.queue,
348            &self.texture_bind_group_layout,
349            model_desc,
350        )
351    }
352
353    pub fn light_handle(&mut self) -> Light {
354        Light
355    }
356
357    /// See [`WindRig::set`]: stores the wind direction/intensity that animates
358    /// the foliage; the GPU upload happens once per frame in `evolbe`.
359    pub fn set_wind(&mut self, direction: [f32; 2], intensity: f32) {
360        self.wind.set(direction, intensity);
361    }
362
363    /// Per-frame engine tick: integrates freshly loaded models and updates the
364    /// camera, light and wind uniforms on the GPU.
365    pub fn evolbe(&mut self) {
366        self.models.collect_loaded();
367
368        let now: web_time::Instant = web_time::Instant::now();
369        let dt: web_time::Duration = now - self.last_render_time;
370        self.last_render_time = now;
371
372        self.camera.update(&self.queue, dt);
373        self.light.update(&self.queue);
374        self.wind.update(&self.queue);
375    }
376}