Skip to main content

ferrum_wgpu/
lib.rs

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