use demomo::gpu::{self, *}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use winit::application::ApplicationHandler; use winit::event::WindowEvent; use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}; use winit::platform::web::{EventLoopExtWebSys, WindowAttributesExtWebSys}; use winit::window::{Window, WindowId}; // An application that draws to the HTML canas element named `name`. pub struct App { name: &'static str, proxy: EventLoopProxy, gpu: Option, scene: Option, surface_cfg: Option, } enum AppEvent { GpuReady(gpu::Gpu), } impl App { async fn new(name: &'static str, event_loop: &EventLoop) -> anyhow::Result { env_logger::init(); console_error_panic_hook::set_once(); let proxy = event_loop.create_proxy(); Ok(Self { name, proxy, gpu: None, scene: None, surface_cfg: None, }) } fn resize(&mut self, width: u32, height: u32) { if width == 0 || height == 0 { self.surface_cfg = None; return; } let Some(gpu) = &mut self.gpu else { return; }; let Some(surface) = &mut gpu.surface else { return; }; let mut cfg = self.surface_cfg.take().unwrap_or_else(|| { surface .get_default_config(&gpu.adapter, width, height) .expect("Incompatible surface") }); cfg.width = width; cfg.height = height; surface.configure(&gpu.device, &cfg); self.surface_cfg = Some(cfg); wgpu::web_sys::console::log_1(&format!("Resize: {}x{}", width, height).into()); } fn make_scene(gpu: &Gpu, surface_cfg: &wgpu::SurfaceConfiguration) -> anyhow::Result { let shader_txt = demomo::include_wgsl!("standard.wgsl"); let shader = Shader::new(&gpu, &shader_txt)?; let vertex_layout = shader.get_vertex_layout()?; let color_target = wgpu::ColorTargetState { format: surface_cfg.format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::all(), }; let pipeline = PipelineBuilder::new(&shader) .vertex_buffer_layout(vertex_layout.vertex_buffer_layout()) .color_target(color_target) .build_render(gpu)?; let positions = [[-0.5_f32, -0.5, 0.5], [0.5, -0.5, 0.5], [0.0, 0.5, 0.5]]; let normals = [[0.0_f32, 0.0, 1.0]; 3]; let uvs = [[0.0_f32, 0.0], [1.0, 0.0], [0.5, 1.0]]; let n_vertices = positions.len(); let data_streams = VertexDataStreamBuilder::new() .stream(VertexSemantic::Position, &positions) .stream(VertexSemantic::Normal, &normals) .stream(VertexSemantic::Uv0, &uvs) .build(); let vertex_buffer = vertex_layout.vertex_buffer(gpu, data_streams, n_vertices)?; let camera = CameraBuilder::default() .eye(glam::Vec3::new(0.0, 0.0, 1.0)) .target(glam::Vec3::ZERO) .up(glam::Vec3::Y) .vertical_fov(60.0_f32.to_radians()) .near(0.1) .far(100.0) .build(gpu, surface_cfg.width as f32 / surface_cfg.height as f32)?; SceneBuilder::new() .object( ObjectBuilder::new() .vertex_buffer(vertex_buffer) .pipeline(pipeline) .camera(&camera) .build(gpu, &shader)?, ) .camera(camera) .build() } fn render(&mut self) -> anyhow::Result<()> { let Some(gpu) = &mut self.gpu else { return Ok(()); }; let Some(surface_cfg) = &mut self.surface_cfg else { return Ok(()); }; let Some(surface) = &gpu.surface else { anyhow::bail!("No surface"); }; let (frame, suboptimal) = match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(frame) => (frame, false), // Suboptimal just means we have to re-configure once done with the texture. // https://docs.rs/wgpu/latest/wgpu/enum.CurrentSurfaceTexture.html#variant.Suboptimal wgpu::CurrentSurfaceTexture::Suboptimal(frame) => (frame, true), wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => { return Ok(()); } wgpu::CurrentSurfaceTexture::Outdated => { surface.configure(&gpu.device, surface_cfg); return Ok(()); } wgpu::CurrentSurfaceTexture::Lost => { let window = gpu.window.as_ref().unwrap().clone(); let surface = gpu.instance.create_surface(window)?; surface.configure(&gpu.device, surface_cfg); gpu.surface = Some(surface); return Ok(()); } wgpu::CurrentSurfaceTexture::Validation => { anyhow::bail!("Surface acquisition failed to validate"); } }; if self.scene.is_none() { self.scene = Some(Self::make_scene(&gpu, surface_cfg)?); } let scene = self.scene.as_ref().unwrap(); scene .camera .update(gpu, surface_cfg.width as f32 / surface_cfg.height as f32); let view = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = gpu .device .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, depth_slice: None, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, g: 0.2, b: 0.4, a: 1.0, }), store: wgpu::StoreOp::Store, }, })], ..Default::default() }); pass.set_pipeline(&scene.objects[0].pipeline); if let Some(camera_bind_group) = &scene.objects[0].camera_bind_group { pass.set_bind_group(0, camera_bind_group, &[]); } pass.set_vertex_buffer(0, scene.objects[0].vertex_buffer.slice(..)); pass.draw(0..3, 0..1); } gpu.queue.submit([encoder.finish()]); gpu.queue.present(frame); if suboptimal { surface.configure(&gpu.device, surface_cfg); } Ok(()) } } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { // Get a canvas to draw on let get_window_attr = || { let window = wgpu::web_sys::window().unwrap_throw(); let document = window.document().unwrap_throw(); let canvas = document.get_element_by_id(&self.name).unwrap_throw(); let html_canvas_element = canvas.unchecked_into(); Window::default_attributes() .with_canvas(Some(html_canvas_element)) .with_prevent_default(false) }; let window_attr = get_window_attr(); // Create a window. let window = event_loop.create_window(window_attr).unwrap_throw(); window.request_redraw(); self.resize(window.inner_size().width, window.inner_size().height); // Have to create Gpu handle asynchronously. On completion, this will // invoke user_event. let proxy = self.proxy.clone(); wasm_bindgen_futures::spawn_local(async move { let gpu = GpuBuilder::new() .window(window) .build() .await .unwrap_throw(); proxy .send_event(AppEvent::GpuReady(gpu)) .unwrap_or_else(|err| panic!("{}", err)); }); } fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { match event { WindowEvent::CloseRequested => { event_loop.exit(); } WindowEvent::Resized(size) => { self.resize(size.width, size.height); self.render().expect("Render failed"); } WindowEvent::RedrawRequested => { self.render().expect("Render failed"); if let Some(window) = self.gpu.as_ref().and_then(|gpu| gpu.window.as_ref()) { window.request_redraw(); } } _ => {} } } fn user_event(&mut self, _: &ActiveEventLoop, event: AppEvent) { match event { AppEvent::GpuReady(gpu) => { let window = gpu.window.as_ref().unwrap().clone(); let size = window.inner_size(); self.gpu = Some(gpu); self.resize(size.width, size.height); window.request_redraw(); } } } } #[wasm_bindgen(start)] pub fn start() { let event_loop = EventLoop::::with_user_event() .build() .unwrap_throw(); wasm_bindgen_futures::spawn_local(async move { let app = App::new("meow", &event_loop).await.unwrap_throw(); event_loop.spawn_app(app); }); }