yum/demomo

A graphics engine built on wgpu.

git clone https://git.yummers.dev/yum/demomo

yumMake camera optionalff99423

master
9.6 KiB274 linesraw
1use demomo::gpu::{self, *};
2use wasm_bindgen::prelude::wasm_bindgen;
3use wasm_bindgen::{JsCast, UnwrapThrowExt};
4use winit::application::ApplicationHandler;
5use winit::event::WindowEvent;
6use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
7use winit::platform::web::{EventLoopExtWebSys, WindowAttributesExtWebSys};
8use winit::window::{Window, WindowId};
9
10// An application that draws to the HTML canas element named `name`.
11pub struct App {
12    name: &'static str,
13    proxy: EventLoopProxy<AppEvent>,
14    gpu: Option<gpu::Gpu>,
15    scene: Option<Scene>,
16    surface_cfg: Option<wgpu::SurfaceConfiguration>,
17}
18
19enum AppEvent {
20    GpuReady(gpu::Gpu),
21}
22
23impl App {
24    async fn new(name: &'static str, event_loop: &EventLoop<AppEvent>) -> anyhow::Result<Self> {
25        env_logger::init();
26        console_error_panic_hook::set_once();
27        let proxy = event_loop.create_proxy();
28        Ok(Self {
29            name,
30            proxy,
31            gpu: None,
32            scene: None,
33            surface_cfg: None,
34        })
35    }
36
37    fn resize(&mut self, width: u32, height: u32) {
38        if width == 0 || height == 0 {
39            self.surface_cfg = None;
40            return;
41        }
42        let Some(gpu) = &mut self.gpu else {
43            return;
44        };
45        let Some(surface) = &mut gpu.surface else {
46            return;
47        };
48
49        let mut cfg = self.surface_cfg.take().unwrap_or_else(|| {
50            surface
51                .get_default_config(&gpu.adapter, width, height)
52                .expect("Incompatible surface")
53        });
54
55        cfg.width = width;
56        cfg.height = height;
57        surface.configure(&gpu.device, &cfg);
58        self.surface_cfg = Some(cfg);
59
60        wgpu::web_sys::console::log_1(&format!("Resize: {}x{}", width, height).into());
61    }
62
63    fn make_scene(gpu: &Gpu, surface_cfg: &wgpu::SurfaceConfiguration) -> anyhow::Result<Scene> {
64        let shader_txt = demomo::include_wgsl!("standard.wgsl");
65        let shader = Shader::new(&gpu, &shader_txt)?;
66
67        let vertex_layout = shader.get_vertex_layout()?;
68        let color_target = wgpu::ColorTargetState {
69            format: surface_cfg.format,
70            blend: Some(wgpu::BlendState::REPLACE),
71            write_mask: wgpu::ColorWrites::all(),
72        };
73        let pipeline = PipelineBuilder::new(&shader)
74            .vertex_buffer_layout(vertex_layout.vertex_buffer_layout())
75            .color_target(color_target)
76            .build_render(gpu)?;
77
78        let positions = [[-0.5_f32, -0.5, 0.5], [0.5, -0.5, 0.5], [0.0, 0.5, 0.5]];
79        let normals = [[0.0_f32, 0.0, 1.0]; 3];
80        let uvs = [[0.0_f32, 0.0], [1.0, 0.0], [0.5, 1.0]];
81        let n_vertices = positions.len();
82        let data_streams = VertexDataStreamBuilder::new()
83            .stream(VertexSemantic::Position, &positions)
84            .stream(VertexSemantic::Normal, &normals)
85            .stream(VertexSemantic::Uv0, &uvs)
86            .build();
87        let vertex_buffer = vertex_layout.vertex_buffer(gpu, data_streams, n_vertices)?;
88
89        let camera = CameraBuilder::default()
90            .eye(glam::Vec3::new(0.0, 0.0, 1.0))
91            .target(glam::Vec3::ZERO)
92            .up(glam::Vec3::Y)
93            .vertical_fov(60.0_f32.to_radians())
94            .near(0.1)
95            .far(100.0)
96            .build(gpu, surface_cfg.width as f32 / surface_cfg.height as f32)?;
97
98        SceneBuilder::new()
99            .object(
100                ObjectBuilder::new()
101                    .vertex_buffer(vertex_buffer)
102                    .pipeline(pipeline)
103                    .camera(&camera)
104                    .build(gpu, &shader)?,
105            )
106            .camera(camera)
107            .build()
108    }
109
110    fn render(&mut self) -> anyhow::Result<()> {
111        let Some(gpu) = &mut self.gpu else {
112            return Ok(());
113        };
114        let Some(surface_cfg) = &mut self.surface_cfg else {
115            return Ok(());
116        };
117        let Some(surface) = &gpu.surface else {
118            anyhow::bail!("No surface");
119        };
120
121        let (frame, suboptimal) = match surface.get_current_texture() {
122            wgpu::CurrentSurfaceTexture::Success(frame) => (frame, false),
123            // Suboptimal just means we have to re-configure once done with the texture.
124            // https://docs.rs/wgpu/latest/wgpu/enum.CurrentSurfaceTexture.html#variant.Suboptimal
125            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => (frame, true),
126
127            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
128                return Ok(());
129            }
130
131            wgpu::CurrentSurfaceTexture::Outdated => {
132                surface.configure(&gpu.device, surface_cfg);
133                return Ok(());
134            }
135
136            wgpu::CurrentSurfaceTexture::Lost => {
137                let window = gpu.window.as_ref().unwrap().clone();
138                let surface = gpu.instance.create_surface(window)?;
139                surface.configure(&gpu.device, surface_cfg);
140                gpu.surface = Some(surface);
141                return Ok(());
142            }
143
144            wgpu::CurrentSurfaceTexture::Validation => {
145                anyhow::bail!("Surface acquisition failed to validate");
146            }
147        };
148
149        if self.scene.is_none() {
150            self.scene = Some(Self::make_scene(&gpu, surface_cfg)?);
151        }
152        let scene = self.scene.as_ref().unwrap();
153        scene
154            .camera
155            .update(gpu, surface_cfg.width as f32 / surface_cfg.height as f32);
156
157        let view = frame
158            .texture
159            .create_view(&wgpu::TextureViewDescriptor::default());
160        let mut encoder = gpu
161            .device
162            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
163        {
164            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
165                label: None,
166                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
167                    view: &view,
168                    depth_slice: None,
169                    resolve_target: None,
170                    ops: wgpu::Operations {
171                        load: wgpu::LoadOp::Clear(wgpu::Color {
172                            r: 0.1,
173                            g: 0.2,
174                            b: 0.4,
175                            a: 1.0,
176                        }),
177                        store: wgpu::StoreOp::Store,
178                    },
179                })],
180                ..Default::default()
181            });
182            pass.set_pipeline(&scene.objects[0].pipeline);
183            if let Some(camera_bind_group) = &scene.objects[0].camera_bind_group {
184                pass.set_bind_group(0, camera_bind_group, &[]);
185            }
186            pass.set_vertex_buffer(0, scene.objects[0].vertex_buffer.slice(..));
187            pass.draw(0..3, 0..1);
188        }
189        gpu.queue.submit([encoder.finish()]);
190        gpu.queue.present(frame);
191
192        if suboptimal {
193            surface.configure(&gpu.device, surface_cfg);
194        }
195
196        Ok(())
197    }
198}
199
200impl ApplicationHandler<AppEvent> for App {
201    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
202        // Get a canvas to draw on
203        let get_window_attr = || {
204            let window = wgpu::web_sys::window().unwrap_throw();
205            let document = window.document().unwrap_throw();
206            let canvas = document.get_element_by_id(&self.name).unwrap_throw();
207            let html_canvas_element = canvas.unchecked_into();
208            Window::default_attributes()
209                .with_canvas(Some(html_canvas_element))
210                .with_prevent_default(false)
211        };
212        let window_attr = get_window_attr();
213        // Create a window.
214        let window = event_loop.create_window(window_attr).unwrap_throw();
215        window.request_redraw();
216        self.resize(window.inner_size().width, window.inner_size().height);
217
218        // Have to create Gpu handle asynchronously. On completion, this will
219        // invoke user_event.
220        let proxy = self.proxy.clone();
221        wasm_bindgen_futures::spawn_local(async move {
222            let gpu = GpuBuilder::new()
223                .window(window)
224                .build()
225                .await
226                .unwrap_throw();
227            proxy
228                .send_event(AppEvent::GpuReady(gpu))
229                .unwrap_or_else(|err| panic!("{}", err));
230        });
231    }
232
233    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
234        match event {
235            WindowEvent::CloseRequested => {
236                event_loop.exit();
237            }
238            WindowEvent::Resized(size) => {
239                self.resize(size.width, size.height);
240                self.render().expect("Render failed");
241            }
242            WindowEvent::RedrawRequested => {
243                self.render().expect("Render failed");
244                if let Some(window) = self.gpu.as_ref().and_then(|gpu| gpu.window.as_ref()) {
245                    window.request_redraw();
246                }
247            }
248            _ => {}
249        }
250    }
251
252    fn user_event(&mut self, _: &ActiveEventLoop, event: AppEvent) {
253        match event {
254            AppEvent::GpuReady(gpu) => {
255                let window = gpu.window.as_ref().unwrap().clone();
256                let size = window.inner_size();
257                self.gpu = Some(gpu);
258                self.resize(size.width, size.height);
259                window.request_redraw();
260            }
261        }
262    }
263}
264
265#[wasm_bindgen(start)]
266pub fn start() {
267    let event_loop = EventLoop::<AppEvent>::with_user_event()
268        .build()
269        .unwrap_throw();
270    wasm_bindgen_futures::spawn_local(async move {
271        let app = App::new("meow", &event_loop).await.unwrap_throw();
272        event_loop.spawn_app(app);
273    });
274}