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// Private 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, Device, PipelineLayout, Queue, RenderPass, RenderPipeline,
22        Surface, SurfaceCapabilities, SurfaceConfiguration, SurfaceTexture, TextureFormat,
23        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 output: SurfaceTexture = match self.window_surface.get_current_texture() {
253            wgpu::CurrentSurfaceTexture::Success(t)
254            | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
255            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
256                return Ok(());
257            }
258            wgpu::CurrentSurfaceTexture::Outdated => return Err(SurfaceError::Outdated),
259            wgpu::CurrentSurfaceTexture::Lost => return Err(SurfaceError::Lost),
260            wgpu::CurrentSurfaceTexture::Validation => return Err(SurfaceError::Validation),
261        };
262
263        let surface_size = output.texture.size();
264        let depth_size = self.depth_texture.texture.size();
265        if surface_size.width != depth_size.width || surface_size.height != depth_size.height {
266            self.resize(surface_size.height, surface_size.width);
267        }
268
269        let mut encoder = self
270            .device
271            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
272                label: Some("encoder"),
273            });
274
275        {
276            let mut shadow_render_pass: RenderPass =
277                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
278                    label: Some("Shadow_render_pass"),
279                    color_attachments: &[],
280                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
281                        view: &self.shadow.texture.view,
282                        depth_ops: Some(wgpu::Operations {
283                            load: wgpu::LoadOp::Clear(1.0),
284                            store: wgpu::StoreOp::Store,
285                        }),
286                        stencil_ops: None,
287                    }),
288                    timestamp_writes: None,
289                    occlusion_query_set: None,
290                    multiview_mask: None,
291                });
292
293            shadow_render_pass.set_pipeline(&self.shadow.pipeline);
294            shadow_render_pass.set_bind_group(0, &self.light.bind_group, &[]);
295            for model in self.models.static_loaded() {
296                shadow_render_pass.draw_shadow_model(model, &self.light.bind_group);
297            }
298        }
299        {
300            let mut render_pass: RenderPass =
301                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
302                    label: Some("render_pass"),
303                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
304                        view: self.hdr.view(),
305                        depth_slice: None,
306                        resolve_target: None,
307                        ops: wgpu::Operations {
308                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
309                            store: wgpu::StoreOp::Store,
310                        },
311                    })],
312                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
313                        view: &self.depth_texture.view,
314                        depth_ops: Some(wgpu::Operations {
315                            load: wgpu::LoadOp::Clear(1.0),
316                            store: wgpu::StoreOp::Store,
317                        }),
318                        stencil_ops: None,
319                    }),
320                    timestamp_writes: None,
321                    occlusion_query_set: None,
322                    multiview_mask: None,
323                });
324
325            render_pass.set_pipeline(&self.light.pipeline);
326            for model in self.models.light_loaded() {
327                render_pass.draw_light_model(
328                    model,
329                    &self.camera.bind_group,
330                    &self.light.bind_group,
331                );
332            }
333
334            render_pass.set_pipeline(&self.render_pipeline);
335            for model in self.models.static_loaded() {
336                render_pass.draw_model(
337                    model,
338                    &self.camera.bind_group,
339                    &self.light.bind_group,
340                    &self.shadow.bind_group,
341                    &self.wind.bind_group,
342                );
343            }
344
345            if let Some(sky) = &self.sky {
346                render_pass.set_pipeline(&sky.pipeline);
347                render_pass.set_bind_group(0, &self.camera.bind_group, &[]);
348                render_pass.set_bind_group(1, &sky.bind_group, &[]);
349                render_pass.draw(0..3, 0..1);
350            };
351        }
352
353        if let Some(sc) = &self.ferrum_config.surface_config {
354            let view: TextureView = output.texture.create_view(&wgpu::TextureViewDescriptor {
355                format: Some(sc.format.add_srgb_suffix()),
356                ..Default::default()
357            });
358            self.hdr.process(&mut encoder, &view);
359            overlay(&self.device, &self.queue, &mut encoder, &view);
360        }
361
362        self.queue.submit(std::iter::once(encoder.finish()));
363        output.present();
364
365        Ok(())
366    }
367
368    pub fn spawn_model(&mut self, model_desc: ModelDesc) -> Ingot<Model> {
369        self.models.spawn(
370            &self.device,
371            &self.queue,
372            &self.texture_bind_group_layout,
373            model_desc,
374            &self.ferrum_config,
375        )
376    }
377
378    pub fn light_handle(&mut self) -> Light {
379        Light
380    }
381
382    pub fn spawn_enviroiment(&mut self, enviroiment: EnviroimentDesc) {
383        self.sky_desc = Some(enviroiment);
384    }
385
386    /// See [`WindRig::set`]: stores the wind direction/intensity that animates
387    /// the foliage; the GPU upload happens once per frame in `evolbe`.
388    pub fn set_wind(&mut self, direction: [f32; 2], intensity: f32) {
389        self.wind.set(direction, intensity);
390    }
391
392    /// Per-frame engine tick: integrates freshly loaded models and updates the
393    /// camera, light and wind uniforms on the GPU.
394    pub fn evolbe(&mut self) {
395        self.models.collect_loaded();
396
397        let now: web_time::Instant = web_time::Instant::now();
398        let dt: web_time::Duration = now - self.last_render_time;
399        self.last_render_time = now;
400
401        if let (Some(desc), Some(sc)) = (self.sky_desc.take(), &self.ferrum_config.surface_config) {
402            // take() → consume y deja None
403            self.sky = Some(
404                SkyRig::new(
405                    &self.device,
406                    &self.queue,
407                    sc,
408                    &self.camera.layout,
409                    self.hdr.format(),
410                    desc,
411                )
412                .expect("Error with load enviroiment"),
413            );
414        }
415
416        self.camera.update(&self.queue, dt);
417        self.light.update(&self.queue);
418        self.wind.update(&self.queue);
419    }
420}