use async_trait::async_trait; use mugl::prelude::*; use munum::{transform, vec3, Mat4, Vec3}; use crate::common::{App, UvVertex, CUBE_INDICES, CUBE_VERTICES}; const PI: f32 = std::f32::consts::PI; #[repr(C)] #[derive(Copy, Clone, Debug)] struct Data { mvp: Mat4, outline: Color, } unsafe impl bytemuck::Pod for Data {} unsafe impl bytemuck::Zeroable for Data {} pub struct StencilExample { size: Extent2D, device: Device, pass: RenderPass, cube: RenderBundle, outline: RenderBundle, sky: RenderBundle, #[allow(dead_code)] image_loaded: bool, #[cfg(all(target_family = "wasm", feature = "backend-webgl"))] image: mugl::webgl::ImageSource, } #[async_trait(?Send)] impl App for StencilExample { #[cfg(feature = "backend-wgpu")] async fn request_wgpu_device(window: &winit::window::Window) -> Option { use mugl::{ wgpu::{WGPUSurfaceDescriptor, WGPU}, Extent2D, }; let size = window.inner_size(); let size = Extent2D(size.width, size.height); WGPU::request_device( window, Default::default(), WGPUSurfaceDescriptor { depth_stencil_format: Some(TextureFormat::DEPTH24STENCIL8), sample_count: 1, size, }, ) .await } fn new(device: Device, size: Extent2D) -> Self { let pass = device.create_render_pass(RenderPassDescriptor::Default { clear_color: Some(Color(0., 0., 0., 1.0)), clear_depth: Some(1.), clear_stencil: Some(0), }); cfg_if::cfg_if! { if #[cfg(all(target_family = "wasm", feature = "backend-webgl"))] { let vertex = include_str!("../shader/stencil.vs.glsl").into(); let fragment = include_str!("../shader/stencil.cube.fs.glsl").into(); } else { let vertex = include_str!("../shader/stencil.vs.wgsl").into(); let fragment = include_str!("../shader/stencil.cube.fs.wgsl").into(); } } let cube = RenderBundle::new( &device, vertex, fragment, true, CompareFunction::LessEqual, CullMode::Back, StencilFaceState { pass_op: StencilOperation::Replace, ..Default::default() }, 0xFF, 1, ); let sky = RenderBundle::new( &device, vertex, fragment, true, CompareFunction::LessEqual, CullMode::Front, Default::default(), 0, 1, ); cfg_if::cfg_if! { if #[cfg(all(target_family = "wasm", feature = "backend-webgl"))] { let vertex = include_str!("../shader/stencil.vs.glsl").into(); let fragment = include_str!("../shader/stencil.outline.fs.glsl").into(); } else { let vertex = include_str!("../shader/stencil.vs.wgsl").into(); let fragment = include_str!("../shader/stencil.outline.fs.wgsl").into(); } } let outline = RenderBundle::new( &device, vertex, fragment, false, CompareFunction::Always, CullMode::Back, StencilFaceState { compare: CompareFunction::NotEqual, ..Default::default() }, 0, 1, ); #[cfg(not(target_family = "wasm"))] { let texture_image = image::load_from_memory(include_bytes!("../assets/airplane.png")).unwrap().into_rgba8(); let texture_size = Extent3D( texture_image.dimensions().0, texture_image.dimensions().1, 1, ); cube.upload_texture_data(&device, &texture_image, texture_size); sky.upload_texture_data(&device, &texture_image, texture_size); } Self { size, device, pass, cube, outline, sky, image_loaded: false, #[cfg(all(target_family = "wasm", feature = "backend-webgl"))] image: mugl::webgl::ImageSource::from_uri("airplane.png"), } } fn device(&self) -> &Device { &self.device } fn render(&mut self, t: f64) -> bool { let t = t as f32; #[cfg(all(target_family = "wasm", feature = "backend-webgl"))] if !self.image_loaded && self.image.size().0 > 0 { self.cube.upload_image(&self.device, &self.image); self.sky.upload_image(&self.device, &self.image); } let aspect = self.size.0 as f32 / self.size.1 as f32; let proj = transform::perspective(aspect, PI / 4.0, 0.1, 100.0); let view = transform::look_at( vec3(10.0 * t.cos(), 5.0 * t.sin(), 10.0 * t.sin()), Vec3::default(), vec3(0., 1., 0.), ); let mut mvp = proj * view; self.cube.update(mvp); mvp *= transform::scaling(vec3(1.1, 1.1, 1.1)); self.outline.update(mvp); mvp *= transform::scaling(vec3(10.0, 10.0, 10.0)); self.sky.update(mvp); { let encoder = self.device.render(&self.pass); self.cube.render(&self.device, &encoder); self.outline.render(&self.device, &encoder); self.sky.render(&self.device, &encoder); encoder.submit(); } self.device.present(); true } fn resize(&mut self, new_size: Extent2D) { self.device.resize_surface(new_size); self.size = new_size; } } struct RenderBundle { pipeline: RenderPipeline, stencil_ref: u32, vertex_buffer: Buffer, index_buffer: Buffer, data_bind_group: BindGroup, data_buffer: Buffer, data: Data, texture_bind_group: Option, texture: Option, #[allow(dead_code)] sampler: Option, } impl RenderBundle { fn new( device: &Device, vertex: &str, fragment: &str, has_texture: bool, depth_compare: CompareFunction, cull_mode: CullMode, stencil: StencilFaceState, stencil_write_mask: u32, stencil_ref: u32, ) -> Self { let data_layout = device.create_bind_group_layout(BindGroupLayoutDescriptor { entries: &[BindGroupLayoutEntry { label: "Data", binding: 0, visibility: ShaderStage::VERTEX | ShaderStage::FRAGMENT, ty: BindingType::Buffer { dynamic_offset: false, }, }], }); let texture_layout = device.create_bind_group_layout(BindGroupLayoutDescriptor { entries: &[ BindGroupLayoutEntry { label: "tex", binding: 0, visibility: ShaderStage::FRAGMENT, ty: BindingType::Texture { multisampled: false, dimension: TextureDimension::D2, sample_type: TextureSampleType::Float, }, }, BindGroupLayoutEntry { label: "tex", binding: 1, visibility: ShaderStage::FRAGMENT, ty: BindingType::Sampler { ty: SamplerBindingType::Filtering, }, }, ], }); let vertex = device.create_shader(ShaderDescriptor { usage: ShaderStage::VERTEX, code: vertex, }); let fragment = device.create_shader(ShaderDescriptor { usage: ShaderStage::FRAGMENT, code: fragment, }); let layout_with_tex = [&data_layout, &texture_layout]; let layout_without_tex = [&data_layout]; let pipeline = device.create_render_pipeline(RenderPipelineDescriptor { vertex: &vertex, fragment: &fragment, buffers: &[VertexBufferLayout { stride: std::mem::size_of::() as BufferSize, step_mode: VertexStepMode::Vertex, attributes: &[ VertexAttribute { offset: 0, shader_location: 0, format: VertexFormat::F32x3, }, VertexAttribute { offset: std::mem::size_of::<[f32; 3]>() as BufferSize, shader_location: 1, format: VertexFormat::F32x2, }, ], }], bind_groups: if has_texture { &layout_with_tex } else { &layout_without_tex }, targets: Default::default(), primitive: PrimitiveState { index_format: Some(IndexFormat::UI16), cull_mode, ..Default::default() }, depth_stencil: Some(DepthStencilState { format: TextureFormat::DEPTH24STENCIL8, depth_write: true, depth_compare, stencil_back: stencil, stencil_front: stencil, stencil_write_mask, stencil_read_mask: 0xFF, ..Default::default() }), multisample: Default::default(), }); let indices_data = bytemuck::cast_slice(CUBE_INDICES); let index_buffer = device.create_buffer(BufferDescriptor { usage: BufferUsage::INDEX, size: indices_data.len() as BufferSize, }); device.write_buffer(&index_buffer, 0, indices_data); let vertices_data = bytemuck::cast_slice(CUBE_VERTICES); let vertex_buffer = device.create_buffer(BufferDescriptor { usage: BufferUsage::VERTEX, size: vertices_data.len() as BufferSize, }); device.write_buffer(&vertex_buffer, 0, vertices_data); let data = [Data { mvp: Mat4::identity(), outline: Color(0.1, 0.3, 0.2, 1.0), }]; let data_raw = bytemuck::cast_slice(&data); let data_buffer = device.create_buffer(BufferDescriptor { usage: BufferUsage::UNIFORM | BufferUsage::STREAM, size: data_raw.len() as BufferSize, }); device.write_buffer(&data_buffer, 0, data_raw); let data_bind_group = device.create_bind_group(BindGroupDescriptor { layout: &data_layout, entries: &[BindGroupEntry { binding: 0, resource: BindingResource::Buffer { buffer: &data_buffer, offset: 0, size: data_raw.len() as BufferSize, }, }], }); let texture = if has_texture { Some(device.create_texture(TextureDescriptor { format: TextureFormat::SRGBA8, size: Extent3D(512, 512, 1), ..Default::default() })) } else { None }; let sampler = if has_texture { Some(device.create_sampler(SamplerDescriptor { address_mode_u: AddressMode::Repeat, address_mode_v: AddressMode::Repeat, mag_filter: FilterMode::Linear, min_filter: FilterMode::Linear, mipmap_filter: FilterMode::Linear, ..Default::default() })) } else { None }; let texture_bind_group = if let Some(ref texture) = texture { if let Some(ref sampler) = sampler { Some(device.create_bind_group(BindGroupDescriptor { layout: &texture_layout, entries: &[ BindGroupEntry { binding: 0, resource: BindingResource::Texture(texture), }, BindGroupEntry { binding: 1, resource: BindingResource::Sampler(sampler), }, ], })) } else { None } } else { None }; Self { pipeline, stencil_ref, vertex_buffer, index_buffer, data_bind_group, data_buffer, data: data[0], texture_bind_group, texture, sampler, } } #[cfg(all(target_family = "wasm", feature = "backend-webgl"))] fn upload_image(&self, device: &Device, image: &mugl::webgl::ImageSource) { if let Some(ref texture) = self.texture { device.copy_external_image_to_texture( ImageCopyExternalImage { src: image, origin: Origin2D(0, 0), }, texture.into(), image.size(), ) } } #[allow(dead_code)] fn upload_texture_data(&self, device: &Device, data: &[u8], texture_size: Extent3D) { if let Some(ref texture) = self.texture { device.write_texture( texture.into(), &data, ImageDataLayout { offset: 0, bytes_per_row: texture_size.0 * 4, rows_per_image: texture_size.1, }, texture_size, ); } } #[inline] fn update(&mut self, mvp: Mat4) { self.data.mvp = mvp; } fn render<'a>(&'a self, device: &Device, pass: &RenderPassEncoder<'a>) { device.write_buffer(&self.data_buffer, 0, bytemuck::cast_slice(&[self.data])); pass.pipeline(&self.pipeline); pass.index(&self.index_buffer); pass.vertex(0, &self.vertex_buffer, 0); pass.bind_group(0, &self.data_bind_group, &[]); self.texture_bind_group.as_ref().map(|texture_bind_group| { pass.bind_group(1, texture_bind_group, &[]); }); pass.stencil_ref(self.stencil_ref); pass.draw_indexed(0..(CUBE_INDICES.len() as u32), 0..1); } }